From 6dd234f146980e32fbd0ef4223c0dad9d23738d7 Mon Sep 17 00:00:00 2001 From: 0xJacky Date: Sun, 2 Aug 2026 10:20:33 +0800 Subject: [PATCH] fix(demo): stop the loading page reloading forever MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two faults, both surfacing as a page that refreshes without end. The Worker checked container readiness and then proxied, which are not atomic: the idle timeout can reap the container in between, leaving the Durable Object reporting a state that is no longer true. The platform then answers with its own "not listening in the TCP address" error, which reached the visitor verbatim instead of the loading page. Detecting that is awkward — it arrives as a plain-text 5xx body rather than a throw, so there is nothing to catch and no status or header that separates it from an application error. Match the message, treat it as not-ready, and fall through to the loading path. The first attempt at this also called recycle() on detection, which was worse than the bug: recycle() stops the container, and by then ready() had already started a new boot, so every request killed the container that was trying to start. Nothing could ever reach healthy and the page refreshed indefinitely. Re-arm the boot, never stop it. Independently, harden the page itself. It reloads when the container reports ready, so a container that flaps made it reload endlessly. Cap reloads within a two-minute window and show an explanation with a manual retry instead. The window matters: spaced-out reloads are ordinary cold starts across a long session and must not accumulate into a false positive. Co-Authored-By: Claude Opus 5 --- cloudflare/src/index.ts | 50 +++++++++++++++++++++++++++++++++++-- cloudflare/src/loading.ts | 52 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 3 deletions(-) diff --git a/cloudflare/src/index.ts b/cloudflare/src/index.ts index 70db8f16..277b7b41 100644 --- a/cloudflare/src/index.ts +++ b/cloudflare/src/index.ts @@ -154,6 +154,30 @@ function isWebSocketUpgrade(request: Request): boolean { return (request.headers.get('upgrade') ?? '').toLowerCase() === 'websocket' } +/** + * Recognise the platform's own "the container went away" response. + * + * When the container is reaped between the readiness check and the proxy, the + * runtime answers with a plain-text error naming the unreachable address rather + * than throwing, so it cannot be caught — it has to be detected. Matching on + * the message is unpleasant but there is no status code or header that + * distinguishes it from an error the application itself produced. + */ +async function isStaleContainerError(response: Response): Promise { + if (response.status < 500 || response.webSocket) { + return false + } + if (!(response.headers.get('content-type') ?? '').startsWith('text/plain')) { + return false + } + + // Peek without consuming: the body is still needed if this turns out to be a + // genuine application error. + const body = await response.clone().text().catch(() => '') + return body.includes('Error proxying request to container') + || body.includes('is not listening in the TCP address') +} + /** * Stamp the public scheme and host onto a request before it reaches the * container. @@ -214,10 +238,32 @@ export default { return container.fetch(withForwardedHeaders(request, url)) } - if (await container.ready()) { + let ready = await container.ready() + + if (ready) { // fetch(), not containerFetch(): only fetch() carries WebSocket upgrades, // which the terminal, log stream and cluster monitor all rely on. - return container.fetch(withForwardedHeaders(request, url)) + const response = await container.fetch(withForwardedHeaders(request, url)) + + // The readiness check and the proxy are not atomic. The idle timeout can + // reap the container in between, leaving the Durable Object reporting a + // state that is no longer true; the platform then answers with its own + // "not listening on ..." error, which used to reach the visitor verbatim. + // Treat that as not-ready, ask for a fresh container, and fall through to + // the loading page the rest of this handler already knows how to serve. + if (await isStaleContainerError(response)) { + // Do NOT stop the container here. It is already gone, and a stop lands + // on whatever boot ready() has since kicked off — every subsequent + // request would kill the container that is trying to start, and the + // loading page would refresh forever. Just re-arm the boot and fall + // through. + console.log('container went away mid-request; waiting for the restart') + await container.ready() + ready = false + } + else { + return response + } } if (wantsDocument(request) && !isWebSocketUpgrade(request)) { diff --git a/cloudflare/src/loading.ts b/cloudflare/src/loading.ts index a61fed25..07c04e22 100644 --- a/cloudflare/src/loading.ts +++ b/cloudflare/src/loading.ts @@ -66,12 +66,60 @@ export function loadingPage(): string {

This instance sleeps when nobody is using it. First request takes a few seconds.

Still starting. This can take up to a minute after a new deploy.

+

+ The demo keeps restarting rather than settling. Reloading has been stopped + so this page does not loop; use the button below to try again. +

+ +