javascript erb ruby 63 lines · 3 tabs

Attach custom headers to Turbo fetch requests (stimulus-free)

Shared by codesnips Jan 2026
3 tabs
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 };
3 files · javascript, erb, ruby Explain with highlit

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

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
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

jwt authentication api
by Kai Nakamura 2 tabs
typescript
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

typescript reliability retry
by codesnips 2 tabs
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

Share this code

Here's the card — post it anywhere.

Attach custom headers to Turbo fetch requests (stimulus-free) — share card
Link copied