ruby erb javascript 40 lines · 3 tabs

Turbo-Location header: redirect a frame submission to a new URL

Shared by codesnips Jan 2026
3 tabs
class SessionsController < ApplicationController
  def new
    @user = User.new
  end

  def create
    @user = User.authenticate_by(email: params[:email], password: params[:password])

    if @user
      start_new_session_for(@user)
      redirect_out_of_frame(after_authentication_url)
    else
      flash.now[:alert] = "Invalid email or password"
      render :new, status: :unprocessable_entity
    end
  end

  private

  def redirect_out_of_frame(url)
    response.headers["Turbo-Location"] = url
    @visit_url = url
    render :create, status: :ok
  end
end
3 files · ruby, erb, javascript Explain with highlit

When a form lives inside a <turbo-frame>, a normal redirect_to after a successful submission is scoped to that frame: Turbo follows the redirect, extracts the matching frame from the response, and swaps only that region. That behavior is usually desirable, but sometimes a successful action should escape the frame entirely — for example, a modal login form that must reload the whole page, or a wizard step that should navigate the browser to a brand-new URL. The Turbo-Location response header is the mechanism for that: it tells Turbo which URL to push into the browser history and, combined with a Turbo Stream that targets the visit, forces a full navigation instead of a frame replacement.

The controller in SessionsController shows the two branches. On failure it re-renders the frame with :unprocessable_entity, keeping the error inside the modal. On success it calls the private redirect_out_of_frame helper, which sets response.headers["Turbo-Location"] to the target path and then renders a Turbo Stream. Setting the header alone is not enough — Turbo only reads it from a response it is already processing, so a stream response is what carries it.

The create.turbo_stream.erb template drives the actual navigation. It uses turbo_stream.action :visit (a custom stream action) so the client performs Turbo.visit(url, { action: "replace" }), replacing the current history entry rather than stacking a new one. Because the visit URL matches the Turbo-Location value, the history stays consistent and a browser refresh lands on the right page.

The turbo_visit_controller.js Stimulus-style action handler registers the visit stream action on the client, since Turbo ships stream actions like replace and append but not visit by default. It reads the url attribute from the <turbo-stream> element and delegates to Turbo.visit.

The key trade-off is that this pattern deliberately breaks frame containment, so it should be reserved for genuine navigations — modals, auth flows, completed wizards — not routine in-frame updates. A common pitfall is setting Turbo-Location on a plain HTML redirect and expecting a full-page visit; without the stream and the registered action, Turbo will still scope the result to the originating frame.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Turbo-Location header: redirect a frame submission to a new URL — share card
Link copied