ruby-on-railsruby

Why do I sometimes get a Type Error "no implicit conversion of StringIO into String" here? Ruby/Rails


I have a line of code in one of my mailers to add an attachment with the surname and DoB of the user. Type Error "no implicit conversion of StringIO into String" sometimes occurs in the File.read method.

attachments["#{@user.last_name.downcase}-#{@user.date_of_birth.strftime('%d%m%y')}.jpeg"] = File.read(URI.parse(@user.profile_picture_url).open)

It works most of the time, but occasionally it just fails with this error. I'm using carrierwave to upload the files to a remote S3 bucket, if that makes a difference.

EDIT:

I've done some digging in the console which has frankly left me more confused! I have two user records, both with profile pictures uploaded via carrierwave to S3. If I just isolate the File.read method and try it using both records one works and one doesn't. Inspecting the URI, they appear to be virtually identical.

However, I have found that carrierwave supports a shortcut to read files which solves the problem. Here's the updated code:

attachments["#{@user.last_name.downcase}-#{@user.date_of_birth.strftime('%d%m%y')}.jpeg"] = @user.profile_picture.read

Solution

  • Anything less than 10kb opens as StringIO instead of File. Couldn't find an actual reference in the docs, so here is the source:

    StringMax = 10240
    def <<(str)
      @io << str
      @size += str.length
    
      # NOTE: once you pass 10kb mark, you get a tempfile.
      #                      v
      if StringIO === @io && StringMax < @size
        require 'tempfile'
        io = Tempfile.new('open-uri')
        io.binmode
        Meta.init io, @io if Meta === @io
        io << @io.string
        @io = io
      end
    end
    

    https://github.com/ruby/ruby/blob/v3_2_0/lib/open-uri.rb#L398


    Let's test it:

    >> require "open-uri"
    
    >> File.write("public/small.txt", "a"*10240)
    => 10240
    >> URI.open("http://localhost:3000/small.txt").class
    => StringIO
    
    >> File.write("public/large.txt", "a"*10241))
    => 10241
    >> URI.open("http://localhost:3000/large.txt").class
    => Tempfile
    

    You can File.read a Tempfile, but you can't File.read a StringIO.

    The fix is to call read on the URI:

    >> URI.parse("http://localhost:3000/small.txt").read.class
    => String
    >> URI.parse("http://localhost:3000/large.txt").read.class
    => String
    
    # also works
    >> URI.open("http://localhost:3000/small.txt").read.class
    => String
    

    https://rubyapi.org/3.2/o/openuri/openread#method-i-read