{"id":12848,"date":"2025-07-16T05:39:34","date_gmt":"2025-07-16T05:39:34","guid":{"rendered":"https:\/\/www.bacancytechnology.com\/qanda\/?p=12848"},"modified":"2025-07-21T06:08:17","modified_gmt":"2025-07-21T06:08:17","slug":"get-attachment-url-without-having-public-urls-in-activestorage","status":"publish","type":"post","link":"https:\/\/www.bacancytechnology.com\/qanda\/ruby-on-rails\/get-attachment-url-without-having-public-urls-in-activestorage","title":{"rendered":"How to Get Attachment URL Without Having Public URLs in ActiveStorage"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>When handling sensitive documents like invoices or contracts in a Rails app, it\u2019s 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.<\/p>\n<p>In this, we&#8217;ll show you how to securely preview or download PDFs from ActiveStorage without generating any public URL &#8211; using a clean, reusable approach.<\/p>\n<h2>Problem Recap<\/h2>\n<p>In many applications, models such as Invoice often include sensitive file attachments like generated PDF documents. For example:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"r\">\r\nclass Invoice &lt; ApplicationRecord\r\n has_one_attached :generated_pdf\r\nend\r\n<\/pre>\n<p>To enhance security, you may disable ActiveStorage\u2019s public routes by configuring the application:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"r\">\r\n# config\/application.rb\r\nconfig.active_storage.draw_routes = false\r\n<\/pre>\n<p>If you attempt to access the file using a direct call like:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"r\">\r\nredirect_to @invoice.generated_pdf.url\r\n<\/pre>\n<p>It results in errors such as:<\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"r\">\r\nCannot generate URL using Disk service...\r\nundefined method `rails_disk_service_url`...\r\n<\/pre>\n<p>This happens because ActiveStorage cannot generate public URLs when using the Disk service or when routes are disabled.<\/p>\n<h2>The Right Way: Serve Secure Files via Controller (Streaming)<\/h2>\n<p>Instead of generating a signed URL, you can stream the file directly from your controller.<\/p>\n<h2>Step-by-Step Solution<\/h2>\n<h3>1. Add Streaming Concern<\/h3>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"r\">\r\n# app\/controllers\/concerns\/streamable_attachment.rb\r\nmodule StreamableAttachment\r\n extend ActiveSupport::Concern\r\n\r\n def stream_attachment(record:, attachment_name:, disposition: \"inline\")\r\n   attachment = record.public_send(attachment_name)\r\n\r\n   return head :not_found unless attachment.attached?\r\n   blob = attachment.blob\r\n   response.headers['Content-Type']        = blob.content_type\r\n   response.headers['Content-Disposition'] = \"#{disposition}; filename=\\\"#{blob.filename}\\\"\"\r\n   response.headers['Content-Length']      = blob.byte_size.to_s\r\n\r\n   blob.download do |chunk|\r\n     response.stream.write chunk\r\n   end\r\n ensure\r\n   response.stream.close\r\n end\r\nend\r\n<\/pre>\n<h3>2. Use It in Your Controller<\/h3>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"r\">\r\nclass InvoicesController < ApplicationController\r\n include StreamableAttachment\r\n\r\n def show_pdf\r\n   invoice = Invoice.find(params[:id])\r\n   # authorize invoice(Use Pundit or your own logic\r\n   stream_attachment(record: invoice, attachment_name: :generated_pdf, disposition: \"inline\")\r\n end\r\nend\r\n<\/pre>\n<h3>3. Add Route<\/h3>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"r\">\r\n# config\/routes.rb\r\nresources :invoices do\r\n member do\r\n   get :show_pdf\r\n end\r\nend\r\n<\/pre>\n<h3>4. Embed PDF in the View<\/h3>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"r\">\r\n<% if @invoice.generated_pdf.attached? %>\r\n  <!-- PDF is attached, but we don't show it -->\r\n<% else %>\r\n  <p>No PDF attached.<\/p>\r\n<% end %>\r\n<\/pre>\n<h2>Why Not Signed URLs?<\/h2>\n<p>Signed URLs are great for performance, but:<\/p>\n<ul>\n<li>Can be forwarded\/shared while still valid<\/li>\n<li>Don't give you control over download logging or rate limiting<\/li>\n<li>Are harder to embed in controlled HTML views<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<ul>\n<li><strong>Avoid Public URLs for Sensitive Files:<\/strong> Exposing file URLs, even signed ones, can pose security risks in applications handling confidential documents.<\/li>\n<li><strong>Use Controller-Based Streaming:<\/strong> Serving attachments via a controller allows you to enforce authentication and authorization before granting access.<\/li>\n<li><strong>Support Inline PDF Viewing:<\/strong> With the correct headers and response setup, PDF files can be previewed directly in the browser, improving user experience.<\/li>\n<li><strong>Keep Code Reusable and Maintainable:<\/strong> Extracting the streaming logic into a reusable concern ensures consistency and reduces duplication across models and controllers.<\/li>\n<li><strong>Scalable for Any Model:<\/strong> This approach is flexible and can be applied to any model with attachments, making it a robust solution for secure file delivery.<\/li>\n<\/ul>\n<div class=\"qanda-read-box\"><div class=\"bg-light read-more-icon\"><img decoding=\"async\" src=\"https:\/\/assets.bacancytechnology.com\/qanda\/wp-content\/uploads\/2025\/04\/24061434\/read-txt.png\" alt=\"Also Read\"><p><\/p><h3>Also Read:<\/h3><a href=\"https:\/\/www.bacancytechnology.com\/blog\/using-rails-active-storage\" target=\"_blank\">Ruby on Rails Active Storage<\/a><\/div><\/div>\n","protected":false},"excerpt":{"rendered":"<p>Introduction When handling sensitive documents like invoices or contracts in a Rails app, it\u2019s 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&#8217;ll show you how to securely preview [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":12849,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[11],"tags":[],"class_list":["post-12848","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ruby-on-rails"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/12848"}],"collection":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/comments?post=12848"}],"version-history":[{"count":5,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/12848\/revisions"}],"predecessor-version":[{"id":12855,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/posts\/12848\/revisions\/12855"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/media\/12849"}],"wp:attachment":[{"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/media?parent=12848"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/categories?post=12848"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.bacancytechnology.com\/qanda\/wp-json\/wp\/v2\/tags?post=12848"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}