lookbook icon indicating copy to clipboard operation
lookbook copied to clipboard

Configuring ActionCable `allowed_request_origins`

Open nevans opened this issue 1 year ago • 3 comments

Describe the bug

Configuring Lookbook's ActionCable (e.g. to add allowed_request_origins) is counter-intuitive and difficult to debug.

To Reproduce

Steps to reproduce the behavior:

  1. Add something like the following to an initializer:
    Rails.application.configure do
      config.action_cable.allowed_request_origins ||= []
      config.action_cable.allowed_request_origins << ExampleApp::Config.default_origin
      config.action_cable.allowed_request_origins << %r{\Ahttps?://localhost(:\d+)?\z}
    end
    
  2. Using one of the origins configured in step one, load Lookbook and view the logs. They will confusingly claim that the origin is not allowed. E.g:
    Request origin not allowed: https://username-name-asldkfjasl-3000.preview.app.github.dev
    
  3. Notice that the websocket path is /lookbook/cable, which helps me realize that Lookbook::Cable has its own configuration: https://github.com/ViewComponent/lookbook/blob/764cf8f2b44ee559a52fcd69cde1c48967d4803b/lib/lookbook/cable/cable.rb#L44-L51
  4. Replace the above configuration with something like the following:
    Lookbook.after_initialize do
      if (config = Lookbook.engine.websocket&.server&.config)
        config.allowed_request_origins ||= []
        config.allowed_request_origins << ExampleApp::Config.default_origin
        config.allowed_request_origins << %r{\Ahttps?://localhost(:\d+)?\z}
      end
    end
    
  5. Reload and see the same error message!?!
  6. After a lot of debugging, realize that engine.websocket.full_mount_path is now /cable, and instead of Lookbook::Cable and Lookbook::Connection it is now ActionCable::Server::Base and ActionCable::Connection::Base.
  7. After some more debugging, realize that accessing Lookbook.engine.websocket from an initializer calls the following method: https://github.com/ViewComponent/lookbook/blob/764cf8f2b44ee559a52fcd69cde1c48967d4803b/lib/lookbook/engine.rb#L119-L121 This is problematic:
    • mount_path comes from config/routes.rb but that loads after the initializers
    • @_websocket memoizes Lookbook::Cable with an empty engine_mount_path

Workaround

Place the following line at the bottom of config/routes.rb:

ActiveSupport::Notifications.instrument "routes_loaded.application"

And place the following into an initializer:

ActiveSupport::Notifications.subscribe "routes_loaded.application" do           
  Lookbook.engine.instance_variable_set(:@_websocket, nil)        
  if (config = Lookbook.engine.websocket&.server&.config)                                                  
    config.allowed_request_origins ||= []                                       
    config.allowed_request_origins << ExampleApp::Config.default_origin
    config.allowed_request_origins << %r{\Ahttps?://localhost(:\d+)?\z}
  end
end

Is there a better approach?

Expected behavior

A simple method for configuring Lookbook's ActionCable::Server::Configuration, documented in the Lookbook documentation. Or, if one exists and is documented elsewhere already, a link to that documentation.

Version numbers

Please complete the following information:

  • Lookbook: 2.0.2
  • ViewComponent: 3.0.0
  • Rails: 6.1.7.3
  • Ruby: 3.0.6

Additional context

I've never configured ActionCable before, so it's entirely possible I simply overlooked a much simpler approach that was documented and I couldn't find it.

To debug the issue, I used rdbg and something like the following:

module LookbookLoggedViewAssigns
  def view_assigns
    if defined?(@engine) && @engine
      logger.info {
        "Lookbook engine: %p" % [{
          mount_path:       @engine.mount_path,
          cable_mount_path: @engine.websocket.full_mount_path,
          config:           @engine.websocket.server.config.allowed_request_origins,
        }]
      }
    end
    super
  end
end

module LookbookLoggedActionCable
  def allow_request_origin?
    logger.info({
      class: self.class,
      server_class: server.class,
      config: server.config,
      mount_path: Lookbook::Engine.mount_path,
      cable_mount_path: Lookbook::Engine.websocket.full_mount_path,
      engine_cable_mount_path: Lookbook.engine.websocket.full_mount_path,
    }.inspect)
    super
  end
end

ActiveSupport.on_load(:action_controller_base) do
  prepend LookbookLoggedViewAssigns
end

ActiveSupport.on_load(:action_cable_connection) do
  prepend LoookbookLoggedActionCable
end

nevans avatar May 23 '23 20:05 nevans