rails: Rails v7: Error on direct upload (ActiveStorage::InvalidDirectUploadTokenError)

Steps to reproduce

Hello, I’m working on a new application with Rails 7 (I started with the release candidate). I’m having an issue with ActiveStorage in which I cannot perform a direct upload.

My configuration is a standard rails app (non-SPA), using the new importmap functionality:

importmap.rb:

pin '@rails/activestorage', to: 'activestorage.esm.js'

I’m doing a POST against http://localhost:3000/rails/active_storage/direct_uploads with following params:

{"blob":{"filename":"IMG_0700.JPG","content_type":"image/jpeg","byte_size":1970730,"checksum":"il85E0GriR/6BKnCtaDmYg=="},"direct_upload_token":"6o-PbMbzQwHxI3pPpUWXXvSCIvjSsR3NEbZhqlowWiZPi5oNF3oBKc-1h-xkRM_yidedFVp22Xf0NvJ66Q7D3g"}

The token is coming from: meta[name="csrf-token"]

And the API is returning me an error 500:

ActiveStorage::InvalidDirectUploadTokenError in ActiveStorage::DirectUploadsController#create

ActiveStorage::InvalidDirectUploadTokenError
Extracted source (around line #35):

#33         ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
#34       rescue ArgumentError
*35         raise ActiveStorage::InvalidDirectUploadTokenError
#36       end
#37 
#38       def direct_upload_token_hmac(session, identifier) # :doc:

Extracted source (around line #31):

#29       end
#30 
*31       def valid_direct_upload_token?(token, attachment_name, session) # :doc:
#32         correct_token = direct_upload_token(session, attachment_name)
#33         ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token)
#34       rescue ArgumentError

Extracted source (around line #21):

#19       decoded_token = token_components.join(SEPARATOR)
#20 
*21       return service_name if valid_direct_upload_token?(decoded_token, attachment_name, session)
#22 
#23       raise ActiveStorage::InvalidDirectUploadTokenError
#24     end


Rails.root: /home/xxx

Framework Trace
activestorage (7.0.0) lib/active_storage/direct_upload_token.rb:35:in `rescue in valid_direct_upload_token?'
activestorage (7.0.0) lib/active_storage/direct_upload_token.rb:31:in `valid_direct_upload_token?'
activestorage (7.0.0) lib/active_storage/direct_upload_token.rb:21:in `verify_direct_upload_token'
activestorage (7.0.0) app/controllers/active_storage/direct_uploads_controller.rb:20:in `verified_service_name'
activestorage (7.0.0) app/controllers/active_storage/direct_uploads_controller.rb:10:in `create'
actionpack (7.0.0) lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'
actionpack (7.0.0) lib/abstract_controller/base.rb:214:in `process_action'
actionpack (7.0.0) lib/action_controller/metal/rendering.rb:53:in `process_action'
actionpack (7.0.0) lib/abstract_controller/callbacks.rb:234:in `block in process_action'
activesupport (7.0.0) lib/active_support/callbacks.rb:118:in `block in run_callbacks'
actiontext (7.0.0) lib/action_text/rendering.rb:20:in `with_renderer'
actiontext (7.0.0) lib/action_text/engine.rb:69:in `block (4 levels) in <class:Engine>'
activesupport (7.0.0) lib/active_support/callbacks.rb:127:in `instance_exec'
activesupport (7.0.0) lib/active_support/callbacks.rb:127:in `block in run_callbacks'
activesupport (7.0.0) lib/active_support/callbacks.rb:138:in `run_callbacks'
actionpack (7.0.0) lib/abstract_controller/callbacks.rb:233:in `process_action'
actionpack (7.0.0) lib/action_controller/metal/rescue.rb:22:in `process_action'
actionpack (7.0.0) lib/action_controller/metal/instrumentation.rb:67:in `block in process_action'
activesupport (7.0.0) lib/active_support/notifications.rb:206:in `block in instrument'
activesupport (7.0.0) lib/active_support/notifications/instrumenter.rb:24:in `instrument'
activesupport (7.0.0) lib/active_support/notifications.rb:206:in `instrument'
actionpack (7.0.0) lib/action_controller/metal/instrumentation.rb:66:in `process_action'
actionpack (7.0.0) lib/action_controller/metal/params_wrapper.rb:259:in `process_action'
activerecord (7.0.0) lib/active_record/railties/controller_runtime.rb:27:in `process_action'
actionpack (7.0.0) lib/abstract_controller/base.rb:151:in `process'
actionview (7.0.0) lib/action_view/rendering.rb:39:in `process'
actionpack (7.0.0) lib/action_controller/metal/live.rb:266:in `block (2 levels) in process'
activesupport (7.0.0) lib/active_support/concurrency/share_lock.rb:162:in `sharing'
activesupport (7.0.0) lib/active_support/dependencies/interlock.rb:37:in `running'
actionpack (7.0.0) lib/action_controller/metal/live.rb:258:in `block in process'
actionpack (7.0.0) lib/action_controller/metal/live.rb:343:in `block in new_controller_thread'

Full Trace
activestorage (7.0.0) lib/active_storage/direct_upload_token.rb:35:in `rescue in valid_direct_upload_token?'
activestorage (7.0.0) lib/active_storage/direct_upload_token.rb:31:in `valid_direct_upload_token?'
activestorage (7.0.0) lib/active_storage/direct_upload_token.rb:21:in `verify_direct_upload_token'
activestorage (7.0.0) app/controllers/active_storage/direct_uploads_controller.rb:20:in `verified_service_name'
activestorage (7.0.0) app/controllers/active_storage/direct_uploads_controller.rb:10:in `create'
actionpack (7.0.0) lib/action_controller/metal/basic_implicit_render.rb:6:in `send_action'
actionpack (7.0.0) lib/abstract_controller/base.rb:214:in `process_action'
actionpack (7.0.0) lib/action_controller/metal/rendering.rb:53:in `process_action'
actionpack (7.0.0) lib/abstract_controller/callbacks.rb:234:in `block in process_action'
activesupport (7.0.0) lib/active_support/callbacks.rb:118:in `block in run_callbacks'
actiontext (7.0.0) lib/action_text/rendering.rb:20:in `with_renderer'
actiontext (7.0.0) lib/action_text/engine.rb:69:in `block (4 levels) in <class:Engine>'
activesupport (7.0.0) lib/active_support/callbacks.rb:127:in `instance_exec'
activesupport (7.0.0) lib/active_support/callbacks.rb:127:in `block in run_callbacks'
activesupport (7.0.0) lib/active_support/callbacks.rb:138:in `run_callbacks'
actionpack (7.0.0) lib/abstract_controller/callbacks.rb:233:in `process_action'
actionpack (7.0.0) lib/action_controller/metal/rescue.rb:22:in `process_action'
actionpack (7.0.0) lib/action_controller/metal/instrumentation.rb:67:in `block in process_action'
activesupport (7.0.0) lib/active_support/notifications.rb:206:in `block in instrument'
activesupport (7.0.0) lib/active_support/notifications/instrumenter.rb:24:in `instrument'
activesupport (7.0.0) lib/active_support/notifications.rb:206:in `instrument'
actionpack (7.0.0) lib/action_controller/metal/instrumentation.rb:66:in `process_action'
actionpack (7.0.0) lib/action_controller/metal/params_wrapper.rb:259:in `process_action'
activerecord (7.0.0) lib/active_record/railties/controller_runtime.rb:27:in `process_action'
actionpack (7.0.0) lib/abstract_controller/base.rb:151:in `process'
actionview (7.0.0) lib/action_view/rendering.rb:39:in `process'
actionpack (7.0.0) lib/action_controller/metal/live.rb:266:in `block (2 levels) in process'
activesupport (7.0.0) lib/active_support/concurrency/share_lock.rb:162:in `sharing'
activesupport (7.0.0) lib/active_support/dependencies/interlock.rb:37:in `running'
actionpack (7.0.0) lib/action_controller/metal/live.rb:258:in `block in process'
actionpack (7.0.0) lib/action_controller/metal/live.rb:343:in `block in new_controller_thread'



Request parameters
{"blob"=>{"filename"=>"IMG_0700.JPG",
 "content_type"=>"image/jpeg",
 "byte_size"=>1970730,
 "checksum"=>"il85E0GriR/6BKnCtaDmYg=="},
 "direct_upload_token"=>"[FILTERED]",
 "direct_upload"=>{"blob"=>{"filename"=>"IMG_0700.JPG",
 "content_type"=>"image/jpeg",
 "byte_size"=>1970730,
 "checksum"=>"il85E0GriR/6BKnCtaDmYg=="},
 "direct_upload_token"=>"[FILTERED]"}}

Session dump
_csrf_token: "-x38kEGvAajnAo6YlDIZsaOjPm1ZpiunN8w-Rn44mQ4"
_direct_upload_token: "UwtB4BnCZH21GN2wJM7fcGcDo7L24MqZimRO8v6WlMY="
session_id: "45f12a183d6afbdf33194251dbac81f8"
warden.user.user.key: [[1], "$2a$12$1WIydVKfCbd1zDKN0WoEqe"]

Env dump
GATEWAY_INTERFACE: "CGI/1.2"
HTTP_ACCEPT: "application/json"
HTTP_ACCEPT_ENCODING: "gzip, deflate"
HTTP_ACCEPT_LANGUAGE: "en-US,en;q=0.5"
HTTP_ORIGIN: "http://localhost:3000"
HTTP_VERSION: "HTTP/1.1"
HTTP_X_CSRF_TOKEN: "6o-PbMbzQwHxI3pPpUWXXvSCIvjSsR3NEbZhqlowWiZPi5oNF3oBKc-1h-xkRM_yidedFVp22Xf0NvJ66Q7D3g"
ORIGINAL_SCRIPT_NAME: ""
REMOTE_ADDR: "127.0.0.1"
SERVER_NAME: "localhost"
SERVER_PROTOCOL: "HTTP/1.1"

Response headers
None

Thank you

Expected behavior

The direct upload controller should accept the token return 200 and upload the image

Actual behavior

The direct upload does not accept the controller and return 500

System configuration

Rails version: 7.0.0

Ruby version: 3.0.3

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 15 (7 by maintainers)

Most upvoted comments

Hello again, So now I understand why it wasn’t working.

Two points I missed:

  • The attachment name wasn’t provided.
  • I was sending the CSRF token instead of the direct upload token.

Both the attachment name and direct upload token can be found in (as @brenogazzola mentioned):

<%= form.file_field :images, direct_upload: true %>

This will output something like:

<input 
  data-target="dropzone.input" 
  data-direct-upload-url="http://localhost:3000/rails/active_storage/direct_uploads" 
  data-direct-upload-attachment-name="caliber#images" 
  data-direct-upload-token="bG9jYWwuObeZv1sTZhMLoY_xbg12GPP6NqwT59PnZnz0dXFBqbY=" 
  type="file" name="caliber[images]" id="caliber_images" disabled="" style="display: none;"
>

From then on, you can use activestorage direct upload with the proper params:

  import { DirectUpload } from '@rails/activestorage';

  new DirectUpload(
    file,
    inputTarget.getAttribute('data-direct-upload-url'),
    inputTarget.getAttribute('data-direct-upload-token'),
    inputTarget.getAttribute('data-direct-upload-attachment-name')
  );

Thank you all for your help

Sure.

module UploadsHelper
  def direct_upload_token(attachment_name = "Account#logo")
    ActiveStorage::DirectUploadToken.generate_direct_upload_token(
      attachment_name,
      ActiveStorage::Blob.service.name,
      session
    )
  end
end

Ok, so here’s how the new direct upload workflow works on v7.0.

Model:

class Photo < ApplicationRecord
  has_one_attached :file
end

Form:

<%= form_with model: @photo do |form| %>
  <%= form.file_field :file, direct_upload: true %>
  <%= form.submit %>
<% end %>

Notice that your form needs a model and the file_field attribute has to match the an attachment name in the model. This will generate the following:

<form action="/photos/913299">
  <input data-direct-upload-url="http://festalab.local:3000/rails/active_storage/direct_uploads" data-direct-upload-attachment-name="photo#file" data-direct-upload-token="YW1hem9uLtxsAgWC2XLx6BLiUPW2MMhj_DTp_3eE_VGlEbYmM0cU" type="file" name="photo[file]" id="photo_file">
  <input type="submit" name="commit" value="Update Photo" data-disable-with="Update Photo">
</form>

Notice that the file field has both data-direct-upload-token and data-direct-upload-attachment-name. When you submit the form, you will do:

upload () {
    const file = this.input.files[0]
    const url = this.input.dataset.directUploadUrl
    const token = this.input.dataset.directUploadToken
    const attachmentName = this.input.dataset.directUploadAttachmentName

    new DirectUpload(file, url, token, attachmentName, this).create((error, blob) => {
   })
}

So basically, the javascript constructor changed to have two more attributes, which are the upload token and the attachment name. Both are generated automatically by field_field helper, as long as you have a valid model and use a valid attachment as the atribute.