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 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:


which in turn is:

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

My uploader looks like this:

# frozen_string_literal: true

class PhotoUploader < Shrine
  # require 'shrine/storage/file_system'
  # storages = {
  #   cache:'public', prefix: 'system/uploads/cache'), # temporary
  #   store:'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


My main Shrine class looks like this:

# frozen_string_literal: true

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


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

Shrine.storages = {
  cache: prefix: 'oll3/cache', **s3_props),
  store: 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,


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

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


  after_save :assign_style

  def name
    super || file_name

  def self.styles
    %w[toc title figure portrait]

  styles.each do |style_name|
    define_method("#{style_name}?".to_sym) do

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

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

  def style_label

  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 ''

  def feature
    default_image? || super

  # 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

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,


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!)


1 Like