🔖

【Rails】Active Storageの画像加工時に出たActiveStorage::FileNotFoundErrorについて

2023/11/03に公開

はじめに

Rails学習時に調べたことや、遭遇したエラーなどについて書いております。
間違いなどありましたら、優しくご指摘いただけましたら幸いです。

対象読者

  • Rails初学者の方
  • ActiveStorage::FileNotFoundErrorが出てしまって困っている方
  • ActiveStorage::Variantのprocessedメソッドについて知りたい方

まずはActive Storageについて概要を知りたい!という方は、よろしければ以下をご覧ください。
https://zenn.dev/meimei_kr/articles/571d350dabfc7e

実行環境

Rails 7.0.4
ruby 3.1.4p223

エラーの発生経緯

実装していたのは、「タイトル」、「内容」、「画像」を投稿する簡単なフォームです。
下書き画面で投稿内容を編集し、「更新する」ボタンを押すと再度下書き画面に戻り、「公開する」ボタンを押すことで公開ページに遷移するような仕様になっています。

Postモデルは以下のように設定しています。Postモデルには1つ画像がアタッチできるようになっています。タイトルに presence: true のバリデーションをつけています。

app/models/post.rb
# == Schema Information
#
# Table name: posts
#
#  id          :bigint           not null, primary key
#  title       :string(255)     not null
#  content     :text(65535)
#  created_at  :datetime         not null
#  updated_at  :datetime         not null
#
# Indexes
# index_posts_on_user_id
#

class Post < ApplicationRecord
  belongs_to :user
  has_one_attached :image

  validates :title, presence: true
end

画像ファイルの表示には、デコレーターに定義しているActive StorageのVariantを使用しています。この例のように複数バージョンの画像を用意したいときには、 processed をつけることで効率的な処理になります。processed メソッドは、対象のvariantがすでに保存されていればそのvariantを返すので、2回目以降のアクセスでは高速になるというメリットがあります。
(※ デコレーターにはDraperではなく、ActiveDecoratorを使用しています。)

app/views/posts/new.html.slim
= simple_form_for @post, url: post_path(@post) do |f|
  = f.input :title
  = f.input :content
  = f.input :image, as: :file
  - if @post.image.attached?
    = image_tag @post.image_url(:thumb)
  = f.button :submit, class: %w[btn btn-primary]
  = link_to '公開する', post_path(@post), class: %w[btn btn-default]
app/decorators/post_decorator.rb
module PostDecorator
  def image_url(version = :origin)
    command = case version
              when :thumb
                { resize_to_limit: [320, 240] }
              when :lg
                { resize_to_limit: [1024, 768] }
              when :ogp
                { resize_to_limit: [120, 630] }
              else
                false
              end

    command ? image.variant(command).processed : image
  end
end

コントローラーの更新関連の箇所の抜粋は以下です。

app/controllers/posts_controller.rb
 before_action :set_post, only: %i[edit update destroy]
 
  def edit; end

  def update
    @post.assign_attributes(post_params)

    if @post.save
      flash[:notice] = '更新しました'
      redirect_to edit_post_url(@post)
    else
      render :edit
    end
  end
  
  private

  def post_params
    params.require(:post).permit(:title, :content, :image)
  end

  def set_post
    @post = Post.find(params[:id])
  end

エラーが発生するのは、以下の操作を行った時です。

  1. 下書き画面で、画像をアップロードする
  2. 必須入力の「タイトル」を空欄にして、バリデーションエラーを発生させ、データが更新されない状態にする
  3. 「更新する」ボタンを押下する

post_deorator.rbの image.variant(command).processed の部分でActiveStorage::FileNotFoundError が発生します。

なぜこのエラーが発生するのか?

ActiveStorage::FileNotFoundError が発生する原因を調べるために、 まずFramework Traceを見てみます。

activestorage (7.0.4) lib/active_storage/service/disk_service.rb:150:in `rescue in stream'
activestorage (7.0.4) lib/active_storage/service/disk_service.rb:143:in `stream'
activestorage (7.0.4) lib/active_storage/service/disk_service.rb:29:in `block in download'
activesupport (7.0.4) lib/active_support/notifications.rb:206:in `block in instrument'
activesupport (7.0.4) lib/active_support/notifications/instrumenter.rb:24:in `instrument'
activesupport (7.0.4) lib/active_support/notifications.rb:206:in `instrument'
activestorage (7.0.4) lib/active_storage/service.rb:163:in `instrument'
activestorage (7.0.4) lib/active_storage/service/disk_service.rb:28:in `download'
activestorage (7.0.4) lib/active_storage/downloader.rb:32:in `download'
activestorage (7.0.4) lib/active_storage/downloader.rb:13:in `block in open'
activestorage (7.0.4) lib/active_storage/downloader.rb:24:in `open_tempfile'
activestorage (7.0.4) lib/active_storage/downloader.rb:12:in `open'
activestorage (7.0.4) lib/active_storage/service.rb:90:in `open'
activestorage (7.0.4) app/models/active_storage/blob.rb:301:in `open'
activestorage (7.0.4) app/models/active_storage/variant.rb:109:in `process'
activestorage (7.0.4) app/models/active_storage/variant.rb:64:in `processed'
以下略

processed がトレースに表示されていますね。この部分のコードを確認したいと思います。
https://github.com/rails/rails/blob/fb6c6007d02114f272c506cb837fff1e7534f413/activestorage/app/models/active_storage/variant.rb#L64-L68
トレースをもう一度見ると、 processed メソッドの次に process メソッドが呼ばれていることがわかります。 process メソッドも確認してみましょう。
https://github.com/rails/rails/blob/fb6c6007d02114f272c506cb837fff1e7534f413/activestorage/app/models/active_storage/variant.rb#L109-L115
次にトレースに記載されているのは、blob.rbの open なので、上記 process メソッド内の blob.open を次に注目します。
blob.open のところでは、実際にファイルを開き、変更を適用しようとしています。
ここで、エラーが発生した経緯を思い出すと、バリデーションエラーが発生し、画面の表示を行おうとした際に、 ActiveStorage::FileNotFoundError が発生してしまっていました。なので、この時点ではDBのBlobのデータは保存されていません。にも関わらず blob.open を実行しているため、エラーが発生してしまっていたということだったんですね。

エラーを解消するには?

現状自分の調べた限りでは、解決方法として2つあるのかなと思っています。
1つ目は、processed を削除することです。削除すると、 processed をつけることによるアクセス効率化のメリットはなくなってしまいますが、エラーは解消されます。

2つ目は、 blob.open が実行される前に、BlobがDBに保存されているかチェックする方法です。
変更点は以下のようになります。
良い方法かはわかりませんが、こちらの方法でもエラーは解消できました。

app/views/posts/new.html.slim
- - if @post.image.attached?
    = image_tag @post.image_url(:thumb)
+ - if @post.image.attached? && @post.image_exists?
    = image_tag @post.image_url(:thumb)
app/decorators/post_decorator.rb
+  def image_exists?
+    image.blob.persisted?
+  end

参考

Discussion