Active Storage Download Link problem

I am saving files to S3. I am attaching to a model. In an email I am presenting a download link to the file. It works for me, but some people are saying they click the link, and nothing happens! What is that from? What can I do to ensure when people click the link, it works. What is the setting in Active Storage to ensure these links work in emails?

my_model.export_file.attach(io: File.open("#{Rails.root}/tmp/export.csv"), filename: "export.csv")
link = Rails.application.routes.url_helpers.rails_blob_url(my_model.export_file, disposition: "attachment")

So the link is sent to the Mailer, and renders nicely there. But apparently fails for some people. Help!

Possible cause:

Active Storage has two ways to generate links: redirect and proxy. When you use the rails_blob_url, you are using whatever is set in resolve_model_to_route configuration option, which by default is the redirect way. And the redirect urls expire after a few minutes.

When you send the email to yourself, you check it immediately, so the url is still valid. When your clients check it, some time has passed and the url expired.

Solutions:

  1. Use the public url: my_model.export_file.url
  2. Use the proxy helper: rails_storage_proxy_url(my_model.export_file)

OK. Thanks for that insight.

I am testing using my :local, and I got this right away. What are url_options?

Export Inventory Error: Cannot generate URL for export.csv using Disk service, please set ActiveStorage::Current.url_options.

I found zero helpful info on this. It is apparently a thing, but there are no examples how to use it. I did the old school ActiveStorage::Current.host = “https://myserver.com” which worked but is frowned upon.

I read up on the Proxy approach. The Guides do not explain how to use that either. I am in a Sidekiq background job with a model and an attachement I need a link to, and the proxy is not a thing at my disposal. I am sure it works, but it sure would be nice to have some examples of actually using it.

This is an annoying problem in disk storage (:local). If you are getting this in a controller:

before_action do
  ActiveStorage::Current.host = request.base_url
end

Anywhere else:

ActiveStorage::Current.set(host: "https://www.example.com") do
  rails_storage_proxy_url(my_model.export_file)
end

That does not work… in my background job.

ActiveStorage::Current.set(host: ENV.fetch("HOSTNAME")) do
  link = rails_storage_proxy_url(my_model.export)
  Rails.logger.debug("Link with concern included would be #{link}")
end

undefined method `rails_storage_proxy_url’

If I change it up a little, it works, but throws the warning! It’s a trap!

ActiveStorage::Current.set(host: ENV.fetch("WEBHOOKS_HOSTNAME")) do
  #link = rails_storage_proxy_url(my_model.export)
  link = my_model.export.url
  Rails.logger.debug("Link with concern included would be #{link}")
end

ActiveStorage::Current.host= is deprecated, instead use ActiveStorage::Current.url_options= (called from block in perform at

Arg! Even these public URLs are expiring. This is terrible. What is the point of Active Storage if it just going to present hassles. It is all nice till it turns bad like this…

Now everybody get this after a bit: AccessDeniedRequest has expired3002022-03-02T20:37:27Z2022-03-03T00:02:56ZZ4T3JDG6B9T6C3G0pX6Rxi6wJ+4OKuo83CZW67LtYKZ9/Y5+b82h+5SsA512QHG6M3n4pwYkNjYVfcyYuPBLUXlrgJA=

Is there not a way to get a link to a file stored in S3 and have it work for as long as that file exists? Why is that so hard to do?

Would you mind sharing the full url to one of the files so I can check it? Also, to get the fully public url:

storage.yml: notice the public config.

amazon:
  service: S3
  access_key_id: my_id
  secret_access_key: my_key
  region: us-east-1
  bucket: my_bucket
  public: true
  http_open_timeout: 5
  http_read_timeout: 5

For reference, this is what each generated url looks like. The code below should work with no additional host or url config in views, controllers and mailers. If you are not getting like any of these 3, there’s probably something wrong.

Public

photo.file.url
# => https://s3.sa-east-1.amazonaws.com/my_bucket/KEY

Redirect

rails_storage_redirect_url photo.file
# => https://example.com/rails/active_storage/blobs/redirect/KEY/file.jpg

Proxy

rails_storage_proxy_url photo.file
# =>https://example.com/rails/active_storage/blobs/proxy/KEY/file.jpg

Thanks for the info. I did not have public set on my storage.yml setting. If I set that, what changes?

The link that expires is here:

https://mybucket.s3.amazonaws.com/xo6u8ec1gaa27h06gzawe8shl4pk?response-content-disposition=attachment%3B%20filename%3D%22export.csv%22%3B%20filename%2A%3DUTF-8%27%27export.csv&response-content-type=text%2Fcsv&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIA4Q55L7RJ34DOBU3M%2F20220302%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20220302T203227Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Signature=a5ece62b3c295b937eb5f643737242fb5f4fc3afd372f6dc62546bd8fc0cc172

So I am in an ActiveJob. I don’t have access to Proxy helpers there. I don’t need the redirect URL as it expires. I need a link that I can rely on in an email, so the person can click and get their report. If setting the public:true makes it so, I guess then the danger is, anyone can access that file. Which kinda sucks, but that is something that I am sure can be taken care of with 19 other steps.

I tested the Proxy URL in a controller, and it worked. So I guess I can always email them a HEY, your report is ready, and then convince them to go into the App and click a download button that offer the Proxy URL.

So you’re saying the Proxy works in Mailers. Not ActiveJobs, but Mailers? Hmmm… that then means I can skip generating the link in the ActiveJob, and just generate in the mail template in the ERB? Interesting. The Mailer has to instantiate the model so it makes sense. I will try that.

I cannot use the public: true setting without deeper understanding. I made an AWS keypair for this bucket that let me upload files. And so those permissions are it. Making the public:true setting in storage.yml resulted in an error from S3 about not being able to make ACLs. Whoa. So I undid that. Then I checked the link made without public: true. Sure enough, it is made for 300 seconds only. I do not know what the result would be if public: true were accepted by S3. Setting permission on buckets requires a PhD… I will never know the one I somehow missed.

So now I am left with the Proxy method as the only viable way to make a link to a file in S3 that is not public: true, but still downloads via the link generated in the App. This is so confusing. Yay!

Ack. Never mind… for better or worse I use this in my ActiveJob:

Rails.application.routes.url_helpers.rails_storage_proxy_url(my_model.export_file)

And I paste that link in the email hoping it lasts.

I agree, there’s not a lot on Rails ActiveStorage Proxy URLS out there. I wrote this: https://www.simplefileupload.com/active-storage-cdn which describes how to use them for public files (you can ignore the cdn part).

This article goes in depth on setting up AWS (scroll down to the AWS section): https://www.simplefileupload.com/rails-file-upload.

Looks like you solved it but maybe this will help!

1 Like

Thanks for that! Much appreciated. I have it working, but there are so many little hiccups and glitches I have encountered, the last week has been fairly difficult as I try and triage as best I can. One thing that seemed to help that I missed was serving up the content_type to the attach method. Without that, The attach was somewhat doomed it seems.

Also, so far, I have found zero explaining how we can intercept requests to expired/deleted blobs via proxy calls! All I was able to do was throw up some words on the public 404 page which is not ideal, as you can imagine. A targeted response to that would be so much nicer.

You can intercept the requests in routes.rb

I use a standalone server to resize and serve the images, so when the request to serve the file comes in I use a custom controller:

  scope ActiveStorage.routes_prefix do
    get "/blobs/proxy/:signed_id/*filename" => "redirect_images#show"
  end

And in my redirect_images controller I have something like this:

class RedirectImagesController < ActiveStorage::BaseController
  include ActiveStorage::SetBlob
  include ActiveStorage::SetHeaders

  def show
    if @blob.image? do some stuff 

I think that’s what you’re asking? I would think you could just rescue from record not found but if you want something more custom this should work.

1 Like

OK. That is cool. I will try that approach out! I am just worried I will break what conventionally happens (a prompt opens for the user to download their CSV)… but if I just add in a rescue to the controller and render some template I make, that’ll be super!