Skip to content

ActiveStorage::Current.host not set causing disk service to throw URI::InvalidURIError #40855

Open

Description

@daqing

Steps to reproduce

config/storage.yml:

test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>
  public: true

config/environments/development.rb:

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

app/models/user.rb:

class User < ApplicationRecord
  validates :username, presence: true

  has_one_attached :avatar

Expected behavior

In rails console:

irb> ActiveStorage::Current.host
=> nil

irb> User.find_by_username('david').avatar.url
=> [should generate permanent url]

Actual behavior

In rails console:

irb> ActiveStorage::Current.host
=> nil

irb> User.find_by_username('david').avatar.url
URI::InvalidURIError (bad URI(is not URI?): nil)

System configuration

Rails version:

Rails 6.1.0

Ruby version:

ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin19]

Activity

  1. daqing commented on Dec 16, 2020

    @daqing
    Author

    There's no documents about setting ActiveStorage::Current.host before generating public permanent url using the Disk service.

  2. timdorr commented on Jan 6, 2021

    @timdorr
    Contributor

    Interestingly, Rails doesn't see this in their test suite because they set this value during setup:

    setup do
    ActiveStorage::Current.host = "https://example.com"
    end

    For Rspec users, you can workaround this by setting it the same way in a spec helper:

    RSpec.configure do |config|
      config.before(:each) do
        ActiveStorage::Current.host = "https://example.com"
      end
    end
  3. hunterbmiller commented on Jan 7, 2021

    @hunterbmiller

    For those using ActionController to handle requests, there is a Concern available that will set ActiveStorage::Current.host to request.base_url as a before_action:

    https://github.com/rails/rails/blob/master/activestorage/app/controllers/concerns/active_storage/set_current.rb

    But, this is only a partial solution as it still wouldn't work outside of your controllers.

    Since I'm using Grape for my request handling, my workaround right now is to just explicitly set ActiveStorage::Current.host before I use .url. Luckily, I only use the Disk service in testing and development.

    It would be ideal if we could set this value in our config, similar to how we can set url options for ActionMailer:

    config.action_mailer.default_url_options = { host: 'localhost:3000' }

    Perhaps there is a better way?

  4. ryanseys commented on Mar 16, 2021

    @ryanseys

    Also seeing this issue. Is there any way to set this host globally e.g. via a configuration, as others have suggested? If that seems reasonable to the maintainers of Rails, I could also take a stab at implementing it myself.

  5. timdorr commented on Mar 16, 2021

    @timdorr
    Contributor

    Presumably #39566 will resolve this by creating a system for URLs outside of the controller context. I'm not sure of a workaround for now, as we punted on our AS work once we ran into this issue.

  6. exocode commented on Apr 1, 2021

    @exocode

    Interestingly, Rails doesn't see this in their test suite because they set this value during setup:

    RSpec.configure do |config|I dadasdf
      config.before(:each) do
        ActiveStorage::Current.host = "https://example.com"
      end
    end

    ^^^ This approach won't work with my rSpec tests.

    rails 6.1.3.1
    Using rspec-support 3.10.2
    Using rspec-core 3.10.1
    Using rspec-expectations 3.10.1
    Using rspec-mocks 3.10.2
    Using rspec 3.10.0
    

    I added your lines in my rails_helper.rb but it is still nil. I also placed ActiveStorage::Current.host = "http://example.com in the model itself, without success:
    WON'T WORK

    class User
      ActiveStorage::Current.host = "http://localhost:3000"
      def main_image_thumb_url
        main_image.variant(thumb_options).url
      end
    end
    

    ONLY when I place that right before calling the .url method, then my tests pass:

    WORKS

    class User
      def main_image_thumb_url
        ActiveStorage::Current.host = "http://localhost:3000"
        main_image.variant(thumb_options).url
      end
    end
    

    What I've done now is placing include ActiveStorage::SetCurrent in my ApplicationController. But I am not sure if this how it should be...

    Normally I expect that this should be set magically in the background.

  7. gat-bryszard commented on Apr 21, 2021

    @gat-bryszard

    I've encountered similar problem, related to behavior of ActiveStorage::Current.host when you use Fibers (we do).

    I'm thinking it might be worth a separate issue. I'm not sure though, so for now mentioning it here.

    Basically the problem is this:

    spec/spec_helper.rb

    config.before do
      ActiveStorage::Current.host = "https://example.com"
    end
    

    And it works fine. Unless we use Fibers:

    example_spec.rb

    it "check ActiveStorage::Current host with Fibers" do
     fiber = Fiber.new do
       Fiber.yield "ActiveStorage::Current:host in Fiber - #{ActiveStorage::Current.host}"
     end
    
     puts fiber.resume
     puts "ActiveStorage::Current:host out of Fiber - #{ActiveStorage::Current.host}"
    end
    

    Result:

    ActiveStorage::Current:host in Fiber - 
    ActiveStorage::Current:host out of Fiber - https://example.com
    

    So now my tests for cases when Fibers are used fail unless I set the ActiveStorage::Current.host = "https://example.com" if Rails.env.test? in the code, right before I'm calling the blob.url. Which is of course awful.

  8. intrip commented on Jun 8, 2021

    @intrip
    Contributor

    @gat-bryszard related to #40855 (comment)

    That's because ActiveStorage::Current uses

    Thread.current[:current_attributes_instances] ||= {}
    Which stores the settings on a Fiber local variable hence when you create a new Fiber you loose this value (Same applies for a new Thread)

    If helps you can workaround this by storing Thread.current[:current_attributes_instances] in a local variable and then restoring it manually:

    it "check ActiveStorage::Current host with Fibers" do
     local_current_attributes = Thread.current[:current_attributes_instances]
     fiber = Fiber.new do
       Thread.current[:current_attributes_instances] = local_current_attributes
       Fiber.yield "ActiveStorage::Current:host in Fiber - #{ActiveStorage::Current.host}"
     end
    
     puts fiber.resume
     puts "ActiveStorage::Current:host out of Fiber - #{ActiveStorage::Current.host}"
    end
  9. intrip commented on Jun 8, 2021

    @intrip
    Contributor

    #39566 would definitely help, until we'll have that feature I was wondering if showing a more meaningful error when ActiveStorage::Current.host is nil would help, @zzak what's your take on this?

  10. jasondoc3 commented on Jun 8, 2021

    @jasondoc3

    For those looking for a quick fix, I've been able to workaround this by monkey patching ActiveStorage::Blob#service_url

    # config/storage.yml
    local:
      service: Disk
      root: <%= Rails.root.join("storage") %>
    module CoreExtensions
      module ActiveStorage
        module Blob
          module ServiceUrl
            def service_url(*)
              if Rails.configuration.active_storage.service == :local
                h = "http://#{Rails.application.routes.default_url_options[:host]}"
                ::ActiveStorage::Current.set(host: h) { super }
              else
                super
              end
            end
          end
        end
      end
    end
    
    # The service_url method does appear to work with the active storage "disk" service
    # This monkey patch is applied in development and test only
    if %w[development test].include?(Rails.env)
      ActiveSupport.on_load(:active_storage_blob) do
        prepend(CoreExtensions::ActiveStorage::Blob::ServiceUrl)
      end
    end
  11. zzak commented on Jun 9, 2021

    @zzak
    Member

    @intrip I think just inheriting the application url would help yes. Alternatively attachment.url should not raise an exception if ActiveStorage::Current.host is nil but a warning instead? 🤷

  12. intrip commented on Jun 9, 2021

    @intrip
    Contributor

    @intrip I think just inheriting the application url would help yes. Alternatively attachment.url should not raise an exception if ActiveStorage::Current.host is nil but a warning instead? 🤷

    @zzak good point, I'd start with a PR which logs a warning and returns nil when .url is not set, ok for you?

  13. zzak commented on Jun 10, 2021

    @zzak
    Member

    @intrip One more question before you start any work, would returning nil have other consequences like on peoples test suites because of the behavior change? 🤔

  14. intrip commented on Jun 10, 2021

    @intrip
    Contributor

    @zzak I've just found #41996, shouldn't it be expressive enough for now?

  15. 16 remaining items

  16. santib commented on Sep 11, 2023

    @santib
    Contributor

    After giving it some thought I think the correct solution would be just replacing ActiveStorage::Current which is a per-thread config, for a global config in the DiskService definition (it only applies here). Something like:

    # config/storage.yml
    
    local:
      service: Disk
      root: <%= Rails.root.join('storage') %>
      url_options:
        protocol: 'http'
        host: 'localhost'
        port: '3000'
    # ...

    thoughts?

  17. JDrizzy commented on Feb 12, 2024

    @JDrizzy

    I encountered this issue when using local disk for tests only. Development and production both use S3.

    To resolve it, instead of including module ActiveStorage::SetCurrent in ApplicationController - which wasn't needed for dev and prod. I've used the lazy load hook on_load in file test_helper.rb, including the module in action_controller e.g.

    # Sets ActiveStorage::Current.host for tests using local disk
    ActiveSupport.on_load :action_controller do
      include ActiveStorage::SetCurrent
    end

    Please let me know if this is not advised. Thanks

  18. mikehowson commented on Feb 29, 2024

    @mikehowson

    Another clean solution is to implement a custom Active Storage service, extending ActiveStorage::Service::DiskService, and overriding #url_options. For instance:

    # lib/active_storage/service/disk_with_host_service.rb
    
    require "active_storage/service/disk_service"
    
    class ActiveStorage::Service::DiskWithHostService < ActiveStorage::Service::DiskService
      def url_options
        Rails.application.default_url_options # or something else for greater flexibility
      end
    end
    # config/storage.yml
    
    local:
      service: DiskWithHost
      root: <%= Rails.root.join("storage") %>

    I believe it is usually okay to do this in test or development environments.

    This fixed it from within rspec for me

  19. mikehulbert commented on Aug 12, 2024

    @mikehulbert

    I also had this issue when trying to access model.my_attachment.url outside of a controller in development. This workaround does the trick for me and I think its neat enough.

    # config/environments/development.rb
    
    config.after_initialize do
      ActiveStorage::Current.url_options = {host: "localhost", port: 3000}
    end
  20. dhh commented on Sep 14, 2024

    @dhh
    Member

    We're seeking to solve this via #42520. Agree it's a problem. There are plenty of times where you want a ActiveStorage url generated outside of the request cycle, and we need to set that url root somewhere.

  21. reopened this on Sep 14, 2024
  22. rails-bot commented on Dec 13, 2024

    @rails-bot

    This issue has been automatically marked as stale because it has not been commented on for at least three months.
    The resources of the Rails team are limited, and so we are asking for your help.
    If you can still reproduce this error on the 8-0-stable branch or on main, please reply with all of the information you have about it in order to keep the issue open.
    Thank you for all your contributions.

  23. reopened this on Jan 7, 2025
  24. reopened this on Jan 7, 2025
  25. natematykiewicz commented on Mar 14, 2025

    @natematykiewicz
    Contributor

    I'm struggling to figure out how to configure this for ActiveJob.

    I'm doing a Turbo Broadcast via an ActiveJob (so outside a request), and my test suite uses the :test ActiveStorage service.

    When running my tests, I keep getting:

    ActionView::Template::Error:
           Cannot generate URL for #{filename} using Disk service, please set ActiveStorage::Current.url_options. for #{partial_path}
    

    I've got include ActiveStorage::SetCurrent in ApplicationController, but that's not helping me for this job, since it's outside of the request.

    In production we use a cloud host via the S3 gem, so that's not an issue there. But these local tests that use the Disk adapter need some way of generating a url via a job.

    I've tried putting this in my config/environments/test.rb, but it didn't help.

    Rails.application.reloader.to_prepare do
      ActiveStorage::Current.url_options = { host: 'https://example.com' }
    end

    Edit 1:

    I think the problem is that ActiveStorage::Current is an ActiveSupport::CurrentAttributes, so it's being wiped every job run. I guess I need to set it somewhere else. Maybe a before_perform in the ApplicationJob.

    Edit 2:

    Yeah, this fixes it:

    class ApplicationJob < ActiveJob::Base
      if Rails.env.test?
        before_perform do
          ActiveStorage::Current.url_options = Rails.application.routes.default_url_options
        end
      end
    end
  26. natematykiewicz commented on Aug 28, 2026

    @natematykiewicz
    Contributor

    I was having troubles with this in a test that returned an ActiveStorage blob url (record.blob.url(expires_in: 1.day)) in a Sidekiq worker. Instead of doing what I suggested above, this in spec/rails_helper.rb works:

    config.before(:each) do
      ActiveStorage::Current.url_options = Rails.application.routes.default_url_options
    end

    It seems strange that this isn't the default though. I understand possibly wanting to override the url options for ActiveStorage, but why not default to the application's default url options?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions