2
1

More than 1 year has passed since last update.

Carrierwave 使用時にNoMethodError: undefined method `closed?' for nil:NilClass が発生した

Posted at

tl;dr

ssrf_filter が 1.1.0 以上の場合にエラーになるので Gemfile に以下を追加し bundle install、もしくは bundle update ssrf_filter を実行する。

Gemfile
gem 'ssrf_filter', '1.0.8'

デバッグ

ある時 carrierwaveremote_*_url= メソッドを使用時にこのエラーに遭遇しました。

Medium.create!(remote_file_url: "https://mgre.co.jp/wp-content/themes/lanchester2017/assets/img/common/noimage1200x630.png")
NoMethodError: undefined method `closed?' for nil:NilClass

環境は ruby 3.0.1、rails 6.1.7、carrierwave 2.2.2 です。

モデルやアップローダーは以下です。一部省略していますが特別なところはありません。

mysql> desc media;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| file       | varchar(255) | NO   |     | NULL    |                |
| created_at | datetime     | NO   |     | NULL    |                |
| updated_at | datetime     | NO   |     | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
class Medium < ApplicationRecord
  mount_uploader :file, MediumUploader
end
class MediumUploader < CarrierWave::Uploader::Base
  process resize_to_limit: [750, 750]
end

ちなみに正確なエラーログは以下です。Net::HTTP が関係するエラーのようです。

NoMethodError: undefined method `closed?' for nil:NilClass
from /Users/kurashita/.rbenv/versions/3.0.1/lib/ruby/3.0.0/net/http/response.rb:340:in `stream_check'

エラーログが少ないので remote_*_url メソッドのソースコードを直接確認していきます。
メソッドの定義箇所は以下のように調べられます。

pry(main)> Medium.new.method(:remote_file_url=).source_location
=> ["/Users/kurashita/work/MGRe/MGRe/vendor/bundle/ruby/3.0.0/gems/carrierwave-2.2.2/lib/carrierwave/orm/activerecord.rb", 18]

ソースを確認すると以下のようになっています。

/Users/kurashita/work/MGRe/MGRe/vendor/bundle/ruby/3.0.0/gems/carrierwave-2.2.2/lib/carrierwave/orm/activerecord.rb
require 'active_record'
require 'carrierwave/validations/active_model'

module CarrierWave
  module ActiveRecord

    include CarrierWave::Mount

    def mount_uploader(column, uploader=nil, options={}, &block)
      super

      mod = Module.new
      prepend mod
      mod.class_eval <<-RUBY, __FILE__, __LINE__+1
        def remote_#{column}_url=(url)
          column = _mounter(:#{column}).serialization_column
          __send__(:"\#{column}_will_change!")
          super
        end
      RUBY
    end
  ...
  end
end

処理の内容的にはほぼ super しているだけなのでオーバーライドされているメソッドを見ていきます。
オーバーライド元のメソッドは以下のように調べることが出来ます。

pry(main)> Medium.new.method(:remote_file_url=).super_method.source_location
=> ["/Users/kurashita/work/MGRe/MGRe/vendor/bundle/ruby/3.0.0/gems/carrierwave-2.2.2/lib/carrierwave/mount.rb", 165]
module CarrierWave
  module Mount

    def mount_uploader(column, uploader=nil, options={}, &block)
      mount_base(column, uploader, options, &block)

      mod = Module.new
      include mod
      mod.class_eval <<-RUBY, __FILE__, __LINE__+1
        ...
        def remote_#{column}_url=(url)
          _mounter(:#{column}).remote_urls = [url]
        end
        ...
      RUBY
    end
  end
end

_mounter(:#{column}).remote_urls= メソッドを呼んでいるので例によってソースを調べていきます。
これを何度か繰り返すので途中割愛し、最終的なメソッドを見つけます。
最終的には以下の download メソッドに行き着きます。

/Users/kurashita/work/MGRe/MGRe/vendor/bundle/ruby/3.0.0/gems/carrierwave-2.2.2/lib/carrierwave/downloader/base.rb
require 'open-uri'
require 'ssrf_filter'
require 'addressable'
require 'carrierwave/downloader/remote_file'

module CarrierWave
  module Downloader
    class Base
      attr_reader :uploader
            ...
      def download(url, remote_headers = {})
        headers = remote_headers.
          reverse_merge('User-Agent' => "CarrierWave/#{CarrierWave::VERSION}")
        uri = process_uri(url.to_s)
        begin
          if skip_ssrf_protection?(uri)
            response = OpenURI.open_uri(process_uri(url.to_s), headers)
          else
            request = nil
            response = SsrfFilter.get(uri, headers: headers) do |req|
              request = req
            end
            response.uri = request.uri
            response.value
          end
        rescue StandardError => e
          raise CarrierWave::DownloadError, "could not download file: #{e.message}"
        end
        CarrierWave::Downloader::RemoteFile.new(response)
      end
      ...
    end
  end
end

ここまで来て SsrfFilter が出てきました。
ssrf_filter では少し前に 1.0.8 から 1.1.0 のバージョンアップが行われているため、以下のどこかが影響している可能性があります。
https://github.com/arkadiyt/ssrf_filter/compare/1.0.8..1.1.0

ただ、今回は急ぎだったため以下のようにバージョンを落として対応しました。

Gemfile
gem 'ssrf_filter', '1.0.8'
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1