import * as Turbo from "@hotwired/turbo";
function metaContent(name) {
const el = document.querySelector(`meta[name="${name}"]`);
return el ? el.getAttribute("content") : null;
}
document.addEventListener("turbo:before-fetch-request", (event) => {
const options = event.detail && event.detail.fetchOptions;
if (!options || !options.headers) return;
const headers = options.headers;
const tenant = metaContent("turbo-tenant");
const requestId = metaContent("turbo-request-id");
const csrf = metaContent("csrf-token");
if (tenant) headers["X-Tenant"] = tenant;
if (requestId) headers["X-Request-Id"] = requestId;
if (csrf && !headers["X-CSRF-Token"]) headers["X-CSRF-Token"] = csrf;
});
export { Turbo };
<!DOCTYPE html>
<html>
<head>
<title><%= content_for(:title) || "Console" %></title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<meta name="turbo-tenant" content="<%= Current.tenant&.slug %>">
<meta name="turbo-request-id" content="<%= SecureRandom.uuid %>">
<%= javascript_importmap_tags %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
</head>
<body>
<%= yield %>
</body>
</html>
class ApplicationController < ActionController::Base
before_action :assign_current_tenant
after_action :echo_request_id
private
def assign_current_tenant
slug = request.headers["X-Tenant"].presence || params[:tenant]
return if slug.blank?
tenant = Tenant.find_by(slug: slug)
head :forbidden and return if tenant.nil?
Current.tenant = tenant
end
def echo_request_id
incoming = request.headers["X-Request-Id"]
response.set_header("X-Request-Id", incoming) if incoming.present?
end
end
This snippet shows how to inject custom HTTP headers into every request Turbo makes — links, form submissions, turbo-frame navigations, and turbo-stream fetches — without wiring up a Stimulus controller. Turbo exposes a turbo:before-fetch-request event that fires just before it issues any fetch, and the event's detail.fetchOptions.headers object is mutable, so plain listeners can augment it in place.
In turbo_headers.js the module attaches a single document.addEventListener for turbo:before-fetch-request. Because the listener is registered on document, it survives Turbo's page-cache swaps and covers frames and streams equally. The handler reads a tenant identifier and a request-tracing token from <meta> tags rendered server-side, then writes them onto headers. It also mirrors the CSRF token, which matters for cross-frame or programmatic navigations where Turbo has not already picked it up. The reason to read from <meta> rather than hard-code values is that the same bundle serves every tenant; the server decides the values per request and the client simply forwards them.
The dispatch guard checks event.detail.fetchOptions before mutating, since a future Turbo version or a custom fetch could omit it. Values that are absent are skipped rather than sent as empty strings, keeping the wire clean.
In layouts/application.html.erb the layout renders the <meta> tags the JavaScript depends on: csrf_meta_tags for the standard Rails token, plus turbo-tenant and a per-request turbo-request-id. SecureRandom.uuid gives each page load a fresh trace id so logs can be correlated across the initial render and its subsequent Turbo fetches.
In application_controller.rb the server closes the loop: an after_action copies the same X-Request-Id onto the response, and a before_action reads X-Tenant from incoming Turbo requests to scope the current tenant. This bidirectional contract — meta tag out, header back — is what makes the tracing id usable end to end.
The trade-off is that this runs for every Turbo request, so the handler must stay cheap and defensive; a thrown error there would break navigation. It is the right tool when a header must accompany all Turbo traffic uniformly, where sprinkling data-turbo attributes per-element would be error-prone.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
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
Share this code
Here's the card — post it anywhere.