Introduction

When handling sensitive documents like invoices or contracts in a Rails app, it’s risky to expose public file URLs, even if they expire. Luckily, Rails allows you to stream attachments through controllers, letting you add authentication and authorization while showing files inline in the browser.

In this, we’ll show you how to securely preview or download PDFs from ActiveStorage without generating any public URL – using a clean, reusable approach.

Problem Recap

In many applications, models such as Invoice often include sensitive file attachments like generated PDF documents. For example:

class Invoice < ApplicationRecord
 has_one_attached :generated_pdf
end

To enhance security, you may disable ActiveStorage’s public routes by configuring the application:

# config/application.rb
config.active_storage.draw_routes = false

If you attempt to access the file using a direct call like:

redirect_to @invoice.generated_pdf.url

It results in errors such as:

Cannot generate URL using Disk service...
undefined method `rails_disk_service_url`...

This happens because ActiveStorage cannot generate public URLs when using the Disk service or when routes are disabled.

The Right Way: Serve Secure Files via Controller (Streaming)

Instead of generating a signed URL, you can stream the file directly from your controller.

Step-by-Step Solution

1. Add Streaming Concern

# app/controllers/concerns/streamable_attachment.rb
module StreamableAttachment
 extend ActiveSupport::Concern

 def stream_attachment(record:, attachment_name:, disposition: "inline")
   attachment = record.public_send(attachment_name)

   return head :not_found unless attachment.attached?
   blob = attachment.blob
   response.headers['Content-Type']        = blob.content_type
   response.headers['Content-Disposition'] = "#{disposition}; filename=\"#{blob.filename}\""
   response.headers['Content-Length']      = blob.byte_size.to_s

   blob.download do |chunk|
     response.stream.write chunk
   end
 ensure
   response.stream.close
 end
end

2. Use It in Your Controller

class InvoicesController < ApplicationController
 include StreamableAttachment

 def show_pdf
   invoice = Invoice.find(params[:id])
   # authorize invoice(Use Pundit or your own logic
   stream_attachment(record: invoice, attachment_name: :generated_pdf, disposition: "inline")
 end
end

3. Add Route

# config/routes.rb
resources :invoices do
 member do
   get :show_pdf
 end
end

4. Embed PDF in the View

<% if @invoice.generated_pdf.attached? %>
  
<% else %>
  

No PDF attached.

<% end %>

Why Not Signed URLs?

Signed URLs are great for performance, but:

  • Can be forwarded/shared while still valid
  • Don't give you control over download logging or rate limiting
  • Are harder to embed in controlled HTML views

Conclusion

  • Avoid Public URLs for Sensitive Files: Exposing file URLs, even signed ones, can pose security risks in applications handling confidential documents.
  • Use Controller-Based Streaming: Serving attachments via a controller allows you to enforce authentication and authorization before granting access.
  • Support Inline PDF Viewing: With the correct headers and response setup, PDF files can be previewed directly in the browser, improving user experience.
  • Keep Code Reusable and Maintainable: Extracting the streaming logic into a reusable concern ensures consistency and reduces duplication across models and controllers.
  • Scalable for Any Model: This approach is flexible and can be applied to any model with attachments, making it a robust solution for secure file delivery.

Need Help With Ruby On Rails Development?

Work with our skilled Ruby on Rails developers to accelerate your project and boost its performance.

Hire Ruby on Rails Developer

Support On Demand!

Related Q&A