The scenario
We're building a review queue for moderators: a list of submissions on the left, and a panel on the right that shows whichever one we click. The panel is a Turbo Frame with the id submission_panel, and each link in the queue targets it. The show action renders its content inside a frame with the same id, and Turbo swaps it in.
The burn
That frame id is a contract: every response a frame request can receive has to contain a matching frame. When one doesn't, Turbo replaces the panel with "Content missing" and throws a TurboFrameMissingError that names the frame but not the URL or the status code.
The contract breaks in two different ways. The first is a bug in our own templates. A new digest submission type was added later, and show.html.erb renders _digest.html.erb for it, which never wraps itself in the frame. The response is a perfectly good 200, with nothing Turbo can use.
The second is every other response the frame might get. Another moderator archives a submission after the queue has loaded, so the scoped lookup in submissions_controller.rb raises, and rescue_from renders the app's friendly not_found.html.erb. Or the session expires, and the request redirects to sign_in.html.erb. Both are full pages with no frame in them. The signed-out case is the worst: the moderator is still looking at the queue, with no sign-in form and no explanation.
Three unrelated causes, one identical symptom.
The fix
We split the causes into bugs to fix and conditions to handle.
The digest is a bug, so we fix it: _digest.html.erb now wraps itself in the submission_panel frame.
The other two will keep happening, so we handle them. In index.html.erb we wrap the frame in an element that listens for turbo:frame-missing. Because the event bubbles, the error message can be ordinary server-rendered markup that starts out hidden. frame_recovery_controller.js cancels Turbo's default and reports the frame id, status, and URL first. Then it recovers: if the response was a redirect, it calls the event's visit so the sign-in page takes over the whole window; anything else shows the inline message and leaves the rest of the page alone.
Why it matters
The obvious fix is a handler that swaps "Content missing" for a friendlier message. That hides the one signal we had. Turbo's default at least throws an error our exception tracker can catch, and a blanket handler turns template bugs into polite messages nobody investigates. So the handler reports before it recovers, and flags a 200 with no frame as a likely template bug.
Error pages are part of the frame contract too: any rescue_from or auth redirect is a response some frame might receive.
Turbo 7.2 turned a missing frame into a full-page visit on its own, so an expired session just worked. Turbo 7.3 replaced that with "Content missing" and an error. Calling visit(response) in a handler puts the old behavior back, one frame at a time.
Read More
- Turbo Handbook: Frames, for how a frame matches its response.
- Turbo Reference: Events, including
turbo:frame-missingand itsresponseandvisitdetails.