Turbo Drive request timeouts: no built-in limit for hung visits and form submissions
0 reputation · 10 Nov 2023, 05:14 UTC
0 reputation · 10 Nov 2023, 05:14 UTC
I'm designing navigation behavior for a Hotwire Turbo application where some endpoints can occasionally hang for a long time. From the documentation, Turbo Drive automatically aborts an in-flight fetch when the user navigates again, and turbo:before-fetch-request lets me intercept or cancel requests. What I cannot find is any documented, built-in network timeout for page visits or form submissions.
My concern is the unresolved state this leaves behind: if a request never resolves, the progress bar keeps showing and a submitted form's submit control stays disabled until the browser's own fetch layer gives up. I would like a bounded wait, after which the visit is aborted, the UI is restored, and the user gets a retry option. I assume I need to wire an AbortController into the fetch options via turbo:before-fetch-request, but I am unsure how that interacts with Turbo's own abort handling on rapid navigation, and whether the same pattern cleanly covers frame navigation and form submissions.
Constraints: this should work across recent Turbo 8.x releases, and I know client-side abort does not stop server-side processing, so idempotency is handled separately.
Is there any supported timeout configuration in current Turbo versions, or is an AbortController listener the intended approach? Does aborting via the fetch request integrate correctly with Turbo's progress bar and form re-enabling? Are there edge cases where Turbo's automatic cancellation conflicts with a manually attached abort signal?
31775 reputation · 10 Nov 2023, 10:32 UTC
There is no documented, built-in network timeout for Turbo Drive visits, frame navigations, or form submissions in Turbo 8.x. An in-flight fetch ends only when the server responds, the browser's network layer fails, or Turbo aborts it because a newer navigation superseded it. The intended approach is exactly what you suspected: attach an AbortSignal to event.detail.fetchOptions inside a turbo:before-fetch-request listener.
Turbo's progress bar and the disabled form submitter are tied to the lifecycle of the fetch request, not to any timer. If the fetch promise never settles, neither does the activity state. Aborting the fetch rejects the promise with an AbortError, which Turbo treats as request failure: the activity state ends, the progress bar hides, and — for forms — the submitter is re-enabled when the submission settles. Submitter re-enabling on error has been refined across 8.x patch releases, so verify this against your installed version rather than assuming it.
const TIMEOUT_MS = 15_000
document.addEventListener("turbo:before-fetch-request", (event) => {
const fetchOptions = event.detail.fetchOptions
// Do not clobber an existing signal; only set one if Turbo hasn't.
if (!fetchOptions.signal) {
fetchOptions.signal = AbortSignal.timeout(TIMEOUT_MS)
}
})
document.addEventListener("turbo:fetch-request-error", (event) => {
// Present your retry UI here instead of failing silently.
})AbortSignal.timeout(ms) requires a reasonably modern browser. For older targets, use an AbortController plus setTimeout, and clear the timer when the request completes.
Turbo's automatic cancellation on rapid navigation uses its own internal AbortController. The one real edge case is the one you flagged: blindly overwriting fetchOptions.signal can detach Turbo's cancellation path. Guard with an existence check (as above) or compose the signals — for example, abort your controller from Turbo's signal via signal.addEventListener("abort", ...). With that handled, the two mechanisms coexist: whichever fires first settles the request, and Turbo's bookkeeping proceeds normally either way.
Turbo Frames and form submissions run through the same fetch pipeline, so one listener covers all three contexts. You can branch on event.target (e.g., a turbo-frame element or a form) to apply different timeouts per context — a shorter bound for frame refreshes, a longer one for form posts.
turbo:fetch-request-error. Wire that listener to your retry UI.@hotwired/turbo version (package.json or importmap) and inspect its bundled FetchRequest source or type definitions for how the signal is consumed.One open detail that would change the recommendation: if any of your hung endpoints are GET visits you expect to be cache-restorable, a timeout abort bypasses Turbo's snapshot cache for that navigation — mention if that applies, since it may argue for a longer bound on visits than on submissions.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.