21 · side path · the error everyone hits

Click edit.

The server has a typo: the edit page wraps its form in <turbo-frame id="message">, no number. The list page asked for message_2. Turbo scans the response, finds no frame with that id, writes Content missing into the frame, and logs an error.

You will see this error. Everyone does. It always means the same thing: the response didn't contain the frame the request came from.

→ click edit on any message
21 · the usual suspects

Three causes, in order of likelihood.

  • The ids don't match. turbo_frame_tag message on one side and turbo_frame_tag "message" on the other. Use the same helper with the same object on both pages and this can't happen.
  • The action redirected somewhere without the frame. The update action sent you to messages_path and that page renders the message without a frame, or under a different id. Follow the redirect in your head and ask: does that page have turbo-frame#message_2?
  • The session expired. The request got redirected to the login page, which has no frames at all. This one is special; next slide.

A fourth, rarer one: the response is a full-page 4xx/5xx error template. Same fix as the login case.

21 · breaking out on purpose

Your session just expired. Click edit.

The server now redirects edit requests to /login. Turbo follows it inside the frame, finds no frame in the login page, and: Content missing. The user sees a broken box instead of a login form.

The login page should say "I need the whole window":

<%# sessions/new.html.erb %>
<% turbo_page_requires_reload %>

<!-- which renders -->
<meta name="turbo-visit-control" content="reload">

With that in place, a frame that lands on the login page breaks out and does a full page load to it. Flip the switch, click edit again.

→ edit (see it fail), switch, edit again
21 · the general hook

For anything else: turbo:frame-missing.

document.addEventListener("turbo:frame-missing", (event) => {
  const { detail: { response, visit } } = event
  event.preventDefault()
  visit(response.url)   // treat it as a page visit instead
})

The event fires on the frame and bubbles. You can turn the miss into a visit, render something else into the frame, or just log it. Most apps never need this; the login case covers nearly everything.

One more thing worth knowing: <turbo-stream> elements inside the matching frame's content get executed when it lands. Anything outside the frame is thrown away with the rest of the response.