Uploading Images to a Rails App via JSON API

The whole process is pretty simple (if you do it rarely enough not to care about performance or data transmitted). Unfortunately, the error messages you might get along the road (both from Paperclip and Carrierwave) aren’t particularly helpful. So here’s a short explanation of a couple possible issues. I’ll assume you’re trying to upload a new avatar image for your User model.

If you use Grape for your API you’ll get the params as a Hashie::Mash. When you just pass the params along to the User model, three things might happen (depending on which gem you’re using to handle the images and the phase of the moon): error message that a Hashie::Mash isn’t supported, error message about an unexpected nil (somewhere, don’t know where) or it can just silently fail.

It’s not really Hashie::Mash’s fault. JSON’s got no support for passing files around. The simplest thing that could possibly work is encoding this image file of yours using Base64 before uploading and pass the resulting string as one of the parameters. Then decode it on the server side into a binary tempfile, which you’ll pass into the User model.

When you do that, you might hit another little bump on this road. If you close and unlink the temporary file to early, neither Paperclip, nor Carrierwave will let you know that something’s wrong. Just another silent fail.

So, here’s some example code that handles that. Good luck!

#  user_api_request_spec.rb
require 'spec_helper'

describe Users do

  describe 'Update user' do
    let(:user) { FactoryGirl.create(:user) }
    let(:image) {
      Rack::Test::UploadedFile.new('spec/fixtures/images/treme.jpg', 'image/jpeg')
    }

    before do
      post "/api/users/#{user.id}", {
          avatar: {
            :content_type => image.content_type,
            :filename => image.original_filename,
            :file_data => Base64.encode64(image.read)
          }
        }.to_json, "Content-Type" => "application/json"
    end

    it 'returns avatar URL' do
      JSON.parse(response.body)['avatar_url'].should_not be_nil
    end
  end
end


# user_update_service.rb
class UserUpdateService

  def initialize(user, params)
    @user = user
    @params = params
  end

  def process
    @params[:avatar] = parse_image_data(@params[:avatar]) if @params[:avatar]
    @user.update!(@params)
  ensure
    clean_tempfile
  end

  private

  def parse_image_data(image_data)
    @tempfile = Tempfile.new('user-avatar')
    @tempfile.binmode
    @tempfile.write Base64.decode64(image_data[:file_data])
    @tempfile.rewind

    ActionDispatch::Http::UploadedFile.new(
      :tempfile => @tempfile,
      :content_type => image_data[:content_type],
      :filename => image_data[:filename]
    )
  end

  def clean_tempfile
    if @tempfile
      @tempfile.close
      @tempfile.unlink
    end
  end

end
Share on Twitter, Facebook
Prev Next