ruby erb javascript 65 lines · 3 tabs

Turbo Streams: partial page auth failure handling

Shared by codesnips Jan 2026
3 tabs
class ApplicationController < ActionController::Base
  before_action :authenticate_user!

  rescue_from ActionController::InvalidAuthenticityToken do
    handle_unauthenticated(reason: :csrf)
  end

  private

  def authenticate_user!
    return if current_user.present?

    handle_unauthenticated(reason: :session)
  end

  def handle_unauthenticated(reason:)
    store_return_location
    flash.now[:alert] = unauthenticated_message(reason)

    respond_to do |format|
      format.html { redirect_to new_session_path, alert: flash.now[:alert] }
      format.turbo_stream do
        @redirect_url = new_session_path
        render "shared/auth_failure", status: :unauthorized
      end
      format.any { head :unauthorized }
    end
  end

  def store_return_location
    return unless request.get? && !request.xhr?

    session[:return_to] = request.fullpath
  end

  def unauthenticated_message(reason)
    reason == :csrf ? "Your session was interrupted. Please sign in again." : "Please sign in to continue."
  end
end
3 files · ruby, erb, javascript Explain with highlit

When a Rails app uses Hotwire's Turbo, form submissions and link clicks are intercepted and sent as fetch requests that expect a Turbo Stream or full-page response. This creates a subtle problem: when a session expires mid-session, a normal redirect_to login_path returns a 302 that Turbo silently follows, and the resulting login page HTML gets swapped into a tiny stream target — producing a broken partial render instead of a clean redirect. The files here show a coherent way to make authentication failures behave correctly inside Turbo requests.

In ApplicationController the entry point is authenticate_user!, called via a before_action. Rather than always redirecting, it inspects request.format and branches through handle_unauthenticated. For HTML it redirects as usual, but for a TURBO_STREAM request it renders a Turbo Stream that replaces a global flash region and, crucially, emits a custom redirect stream action. The status: :unauthorized code and Turbo-Frame awareness keep Turbo from treating the body as a normal navigation.

The auth_failure.turbo_stream.erb template is the payload. It uses turbo_stream.replace to update the flash and a custom <turbo-stream action="redirect"> element carrying the target URL in a data attribute. Turbo does not know this action natively, so the browser side must teach it.

That is the role of redirect_controller.js, a Stimulus-style registration of a custom StreamActions.redirect. When Turbo processes the stream it invokes this function, which reads getAttribute("url") and performs Turbo.visit(...) with action: "replace", giving a real navigation to the login page rather than a fragment swap.

The trade-off is a small amount of client code in exchange for correct behavior across every Turbo interaction, including frames and streams. A common pitfall is forgetting the :unauthorized status — without it some flows still try to merge the body. Another is CSRF: expired sessions often also invalidate the token, so rescue_from ActionController::InvalidAuthenticityToken is routed through the same handler for consistency. This pattern is worth reaching for whenever an app mixes long-lived pages with Turbo-driven updates and needs session expiry to feel seamless rather than broken.


Related snips

Share this code

Here's the card — post it anywhere.

Turbo Streams: partial page auth failure handling — share card
Link copied