heartcombo / devise

Flexible authentication solution for Rails with Warden.
http://blog.plataformatec.com.br/tag/devise/
MIT License
23.95k stars 5.55k forks source link

UncaughtThrowError(uncaught throw :warden) when using timeoutable with ApplicationCable #5419

Open reinisla opened 2 years ago

reinisla commented 2 years ago

Same issue as UncaughtThrowError warden #4427

When ApplicationCable Subscription to channel is active and timeout_in is set to x minutes, when the timeout kicks in ApplicationCable will try to stream to channel, trigger find_verified_user and fail with UncaughtThrowError(uncaught throw :warden)

Environment

devise.rb

  config.timeout_in = 20.minutes

Current behavior

Default setup for ApplicationCable Connection

# frozen_string_literal: true

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private def find_verified_user
      env["warden"].user or reject_unauthorized_connection
    end
  end
end

If you have an active channel subscription and Devise timeout kicks in

env["warden"].user

will fail with

  ↳ app/channels/application_cable/connection.rb:12:in `find_verified_user'
There was an exception - UncaughtThrowError(uncaught throw :warden)
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/devise-4.8.0/lib/devise/hooks/timeoutable.rb:28:in `throw'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/devise-4.8.0/lib/devise/hooks/timeoutable.rb:28:in `block in <main>'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/warden-1.2.9/lib/warden/hooks.rb:15:in `block in _run_callbacks'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/warden-1.2.9/lib/warden/hooks.rb:10:in `each'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/warden-1.2.9/lib/warden/hooks.rb:10:in `_run_callbacks'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/warden-1.2.9/lib/warden/manager.rb:52:in `_run_callbacks'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/warden-1.2.9/lib/warden/proxy.rb:191:in `set_user'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/warden-1.2.9/lib/warden/proxy.rb:229:in `user'
/Users/reinis/sites/abtion/bladkompagniet/app/channels/application_cable/connection.rb:12:in `find_verified_user'
/Users/reinis/sites/abtion/bladkompagniet/app/channels/application_cable/connection.rb:8:in `connect'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/connection/base.rb:173:in `handle_open'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/server/worker.rb:60:in `block in invoke'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/server/worker.rb:42:in `block in work'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/callbacks.rb:117:in `block in run_callbacks'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/server/worker/active_record_connection_management.rb:16:in `block in with_database_connections'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/connection/tagged_logger_proxy.rb:24:in `block in tag'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/tagged_logging.rb:99:in `block in tagged'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/tagged_logging.rb:37:in `tagged'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/tagged_logging.rb:99:in `tagged'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/connection/tagged_logger_proxy.rb:24:in `tag'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/server/worker/active_record_connection_management.rb:16:in `with_database_connections'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/callbacks.rb:126:in `block in run_callbacks'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/engine.rb:62:in `block (4 levels) in <class:Engine>'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/execution_wrapper.rb:88:in `wrap'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/engine.rb:57:in `block (3 levels) in <class:Engine>'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/callbacks.rb:126:in `instance_exec'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/callbacks.rb:126:in `block in run_callbacks'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/activesupport-6.1.4.1/lib/active_support/callbacks.rb:137:in `run_callbacks'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/server/worker.rb:41:in `work'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/server/worker.rb:59:in `invoke'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/actioncable-6.1.4.1/lib/action_cable/server/worker.rb:54:in `block in async_invoke'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/concurrent-ruby-1.1.9/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:363:in `run_task'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/concurrent-ruby-1.1.9/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:352:in `block (3 levels) in create_worker'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/concurrent-ruby-1.1.9/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:335:in `loop'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/concurrent-ruby-1.1.9/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:335:in `block (2 levels) in create_worker'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/concurrent-ruby-1.1.9/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:334:in `catch'
/Users/reinis/.rvm/gems/ruby-3.0.2/gems/concurrent-ruby-1.1.9/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb:334:in `block in create_worker'

To reproduce:

Expected behavior

Does not throw UncaughtThrowError(uncaught throw :warden)

Temporary solution (rescue exception)

# frozen_string_literal: true

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user

      reject_unauthorized_connection if current_user.blank?
    end

    private def find_verified_user
      env["warden"].user
    rescue UncaughtThrowError
      nil
    end
  end
end
matiasmelendi commented 1 year ago

Hi guys, I'm experiencing the exact same error, is there any solution for that? Is catching the error the right way to "solve" it?

bessey commented 1 year ago

Also running into this. Its not a huge thing but a slightly more precise temporary solutions is:

user = swallow_warden_throws { env["warden"].user }

def swallow_warden_throws
  catch(:warden) do
    result = yield
    # This might look pointless, but it's not. We only get here if we don't throw, so we need to return early to
    # expose the yielded value to the caller as opposed to nil
    return result
  end
  nil
end

This avoids rescuing unrelated throw errors.

stevereinke commented 11 months ago

Is there any progress here? Would be nice to get this merged...

nhorton commented 9 months ago

Just came across this same issue. Thanks to the folks above for at least putting in workarounds.