Suddenly, one of my uploaders is failing with an IO error

I have three nearly identical uploaders in one application, using Shrine 3.2.1. Today, I got a report that uploading an Image (as opposed to a File or Media) was not working. I tried it myself, and get the following error in console:

Started POST "/images" for 173.161.197.6 at 2020-11-16 15:53:10 +0000
Processing by ImagesController#create as HTML
  Parameters: {"authenticity_token"=>"hs93rPcV+5AHOW9Iocpe8/1IuDaQJ+C2IZhZiA2r3y8iP8FE/xy4d5JEZW02WqaKKrPGj4Eq2la3m2El8rQNzw==", "image"=>{"file"=>#<ActionDispatch::Http::UploadedFile:0x0000558136e32be8 @tempfile=#<Tempfile:/tmp/RackMultipart20201116-28329-2la468.jpg>, @original_filename="women on liberty.jpg", @content_type="image/jpeg", @headers="Content-Disposition: form-data; name=\"image[file]\"; filename=\"women on liberty.jpg\"\r\nContent-Type: image/jpeg\r\n">, "name"=>"", "feature"=>"0", "style"=>"art", "caption"=>""}, "commit"=>"Create Image"}
Completed 500 Internal Server Error in 830ms (ActiveRecord: 2.0ms | Allocations: 18583)
  
IOError (closed stream):
  
app/controllers/images_controller.rb:22:in `create'

That line is:

    @image.assign_attributes(image_params)

which in turn is:

  def image_params
    params.require(:image).permit(:name, :file, :feature, :caption, :style)
  end

My uploader looks like this:

# frozen_string_literal: true

class PhotoUploader < Shrine
  # require 'shrine/storage/file_system'
  #
  # storages = {
  #   cache: Shrine::Storage::FileSystem.new('public', prefix: 'system/uploads/cache'), # temporary
  #   store: Shrine::Storage::FileSystem.new('public', prefix: 'system/uploads/store'), # permanent
  # }
  plugin :remove_attachment
  plugin :metadata_attributes, filename: :name
  plugin :determine_mime_type

  def generate_location(io, record: nil, derivative: nil, **)
    return "loose/#{super}" unless record&.persisted?

    table  = record.attachable&.class&.table_name || 'loose'
    id     = record.attachable_id || 'images'
    file   = record.file_name

    "#{table}/#{id}/#{file}"
  end
end

My main Shrine class looks like this:

# frozen_string_literal: true

require 'shrine'
require 'shrine/storage/s3'

Shrine.plugin(:activerecord)
Shrine.plugin(:cached_attachment_data)

s3_props = { public: true,
  bucket: 'REDACTED',
  region: 'REDACTED',
  access_key_id: 'REDACTED',
  secret_access_key: 'REDACTED' 
}

Shrine.storages = {
  cache: Shrine::Storage::S3.new( prefix: 'oll3/cache', **s3_props),
  store: Shrine::Storage::S3.new( prefix: 'oll3/store', **s3_props),
}

I’m pretty sure that’s all fine, because it works in other places (other children of this class are working fine).

Can anyone see anything obvious that I’m missing here? Are there any tricks I can use to get further visibility into the error? IOError (closed stream) is fairly opaque. Which stream is it referring to? Cache? Store? Is this happening while trying to read back the file for metadata extraction? While moving from cache to store?

Thanks in advance,

Walter

One more wrinkle: this same uploader works fine if I use it in a nested form context. I can upload an image when it has a (polymorphic) parent record. I’ve set that relationship to optional: true (and I also tried required: false) but that doesn’t seem to kick anything free.

Here’s the Image model, which I realize I left out of my initial query.

# frozen_string_literal: true

class Image < ApplicationRecord
  belongs_to :attachable, polymorphic: true, optional: true
  has_many :pages, dependent: :nullify
  include PhotoUploader::Attachment.new(:file)

  scope :portraits, -> { where(attachable_type: 'Person') }
  scope :title_page, -> { where(style: 'title') }

  strip_attributes

  after_save :assign_style

  def name
    super || file_name
  end

  def self.styles
    %w[toc title figure portrait]
      .concat(unscoped.distinct(:style).pluck(:style))
      .compact
      .uniq
      .sort
  end

  styles.each do |style_name|
    define_method("#{style_name}?".to_sym) do
      style&.match?(/\.#{style_name}\z/i)
    end
  end

  def default_image?
    %w[toc title].include?(style)
  end

  def assign_style
    update_column(:style, image_style) if style.blank?
  end

  def style_label
    style.to_s.style_label
  end

  def image_style
    return unless file_name?

    case file_name.to_s
    when /_ToC/ then 'toc'
    when /_TP/ then 'title'
    when /_figure/ then 'figure'
    else ''
    end
  end

  def feature
    default_image? || super
  end

  # build a set of format checkers like @image.png? or @image.gif?
  cattr_reader :formats, { instance_accessor: false, default: %w[gif jpeg jpg png].freeze }

  formats.each do |format|
    define_method("#{format}?".to_sym) do
      file_name&.match?(/\.#{format}\z/i)
    end
  end
end

The fact that this works just fine in a nested child record context means the issue is not Shrine, but rather my use of Rails. Any thoughts about how this is set up from that perspective?

Thanks again,

Walter

This error means that the ActionDispatch uploaded file from the params was for some reason closed when it was given to Shrine.

Are you by any chance assigning the attributes twice? Maybe some of these comments might help.

Aha. Yes, now that you mention it, I am using CanCanCan and its load_and_authorize_resource, which happily does the attribute assignments for me without any code in the controller. So my assign_attributes was a second bite at the same apple.

Thanks again for the help! (And Shrine!)

Walter

1 Like