Skip to content

Frame-redirect following

When a request made by a Turbo Frame gets a redirect, Turbo follows it and loads the target inside the frame. After a form submit, you usually want the whole page to go there instead. TurboFrameListener makes that the default, without any change to your controllers:

  1. Turbo FramePOST, Turbo-Frame: project-edit
  2. Controller302, Location: /projects/1
  3. TurboFrameListener204, Turbo-Location: /projects/1
  4. Browserfull Drive visit of /projects/1

The browser side is the turbo:before-fetch-response listener of TurboHandler: it reads Turbo-Location, cancels the frame render and visits the URL.

The listener subscribes to kernel.response. It replaces the response with an empty 204 No Content carrying a Turbo-Location header set to the original Location when:

  1. the response is a redirection (3xx) with a Location header: a 304 Not Modified (a frame revalidated with an ETag) is left alone,
  2. and the request has no Turbo-Frame-Follow-Redirect header (the frame keeps its redirects),
  3. and one of:
    • the request has a Turbo-Frame header,
    • the request is a DELETE with an X-Turbo-Request-Id header (sent by Turbo 8 with its requests, not by Turbo 7), and follow_delete_redirects is on.

The 204 keeps the headers of the redirect response, cookies and Cache-Control included: only the status, the Location header (moved to Turbo-Location) and the body headers (Content-Type, Content-Length) change. Flash messages live in the session, whose cookie Symfony adds later in kernel.response.

Any other response passes through unchanged.

A 302 answered to a DELETE fetch is followed by the browser with the same DELETE method, on the redirect target. Converting the redirect into a Turbo-Location visit avoids that, for frame and non-frame Turbo requests alike, except for frames that keep their redirects.

Turn it off with the follow_delete_redirects option, or the followDeleteRedirects constructor argument without the framework:

config/packages/silarhi_turbo.yaml
silarhi_turbo:
follow_delete_redirects: false

Add data-turbo-follow-redirect to a <turbo-frame> to opt it out of the escalation: the handler sends a Turbo-Frame-Follow-Redirect header with that frame’s requests, and TurboFrameListener leaves all their redirects untouched, DELETE ones included. See Keeping a redirect in its frame.

Something in the browser must follow the Turbo-Location header: on its own, Turbo receives an empty 204 and the frame does not change. If you don’t use TurboHandler, add the equivalent listener:

import * as Turbo from '@hotwired/turbo'
document.addEventListener('turbo:before-fetch-response', (event) => {
const location = event.detail.fetchResponse.response.headers.get('Turbo-Location')
if (location) {
event.preventDefault()
Turbo.visit(location)
}
})

The HTTP headers reference lists the whole contract between the two halves.