From b50a6ab426b1104e03fceaac59cc2b18274f19e9 Mon Sep 17 00:00:00 2001 From: xrpbanks <126300068+xrpbanks@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:15:30 +0200 Subject: [PATCH] fix(login): request a sandboxed document during Render startup A user navigation successfully woke the backend after background health requests had timed out. Request one isolated health document during warmup, and retain the existing JSON readiness and retry checks. Restrict the document to a configured HTTPS Render origin and remove it on success, timeout or cancellation. Tests and production build pass; a real cold-start check remains required. --- frontend/lib/backendRequest.ts | 49 ++++++++++ .../tests/backend_warmup_rate_limit.test.mjs | 89 ++++++++++++++++++- 2 files changed, 137 insertions(+), 1 deletion(-) diff --git a/frontend/lib/backendRequest.ts b/frontend/lib/backendRequest.ts index 2bc9310..1827e4d 100644 --- a/frontend/lib/backendRequest.ts +++ b/frontend/lib/backendRequest.ts @@ -140,6 +140,53 @@ async function discardResponseBody(response: Response) { } } +function mountBackendWakeDocument(backendBaseUrl: string): () => void { + const noop = () => {}; + if ( + typeof document === "undefined" || + typeof window === "undefined" || + !document.body + ) { + return noop; + } + + let backend: URL; + try { + backend = new URL(backendBaseUrl); + } catch { + return noop; + } + if ( + backend.protocol !== "https:" || + !backend.hostname.endsWith(".onrender.com") || + backend.origin === window.location.origin || + backend.username || backend.password || backend.port || + backend.pathname !== "/" || backend.search || backend.hash + ) { + return noop; + } + + // A normal browser document can wake a sleeping Render service while + // background fetches receive startup responses. This makes one document + // request, with scripts and navigation disabled. Its load event is never + // treated as readiness: the regular health probes must still confirm JSON. + const frame = document.createElement("iframe"); + frame.hidden = true; + frame.tabIndex = -1; + frame.title = "CalorieApp startup"; + frame.referrerPolicy = "no-referrer"; + frame.setAttribute("aria-hidden", "true"); + frame.setAttribute("sandbox", ""); + frame.src = `${backend.origin}/health`; + try { + document.body.appendChild(frame); + } catch { + frame.remove(); + return noop; + } + return () => frame.remove(); +} + /** * Render free services can take 50 seconds or more to wake after inactivity. * Probe one health route until the backend returns the expected JSON response, @@ -243,6 +290,7 @@ export async function waitForBackendReady( } throwIfAborted(signal); + const removeWakeDocument = mountBackendWakeDocument(normalizedBaseUrl); const directController = new AbortController(); const sameOriginController = new AbortController(); const abortBoth = () => { @@ -272,6 +320,7 @@ export async function waitForBackendReady( throw new BackendRequestTimeoutError(); } finally { abortBoth(); + removeWakeDocument(); signal?.removeEventListener("abort", abortBoth); } } diff --git a/tools/tests/backend_warmup_rate_limit.test.mjs b/tools/tests/backend_warmup_rate_limit.test.mjs index 48d0409..a8032b8 100644 --- a/tools/tests/backend_warmup_rate_limit.test.mjs +++ b/tools/tests/backend_warmup_rate_limit.test.mjs @@ -33,7 +33,7 @@ function response(status, retryAfter = null, contentType = "text/plain") { const healthy = () => response(200, null, "application/json"); -function warmupHarness(responses) { +function warmupHarness(responses, browser = {}) { let now = Date.UTC(2026, 0, 1); const startedAt = now; let nextTimer = 0; @@ -45,10 +45,12 @@ function warmupHarness(responses) { } const context = vm.createContext({ AbortController, + URL, Date: Clock, module, exports: module.exports, process: { env: {} }, + ...browser, setTimeout(callback, delay) { const id = ++nextTimer; timers.set(id, { at: now + delay, callback }); @@ -102,6 +104,91 @@ function warmupHarness(responses) { }; } +function browserDocument() { + const mounted = new Set(); + const navigations = []; + return { + mounted, + navigations, + window: { location: { origin: "https://app.calorietoken.net" } }, + document: { + body: { + appendChild(frame) { + mounted.add(frame); + navigations.push(frame); + }, + }, + createElement(tag) { + assert.equal(tag, "iframe"); + return { + attributes: {}, + setAttribute(name, value) { this.attributes[name] = value; }, + remove() { mounted.delete(this); }, + }; + }, + }, + }; +} + +test("a document request wakes a backend that background fetches cannot wake", async () => { + const browser = browserDocument(); + const harness = warmupHarness(() => + browser.navigations.length && harness.elapsed() >= 45_000 + ? healthy() + : response(429, "0"), browser); + await harness.settle(harness.start(undefined, 180_000, "https://backend.onrender.com")); + assert.equal(browser.navigations.length, 1); + assert.equal(browser.navigations[0].src, "https://backend.onrender.com/health"); + assert.equal(browser.navigations[0].attributes.sandbox, ""); + assert.equal(browser.navigations[0].referrerPolicy, "no-referrer"); + assert.equal(browser.navigations[0].hidden, true); + assert.equal(browser.mounted.size, 0); + assert.equal(harness.elapsed(), 60_000); +}); + +test("a document load alone never counts as a healthy backend", async () => { + const browser = browserDocument(); + const harness = warmupHarness(() => response(429, "0"), browser); + await assert.rejects( + harness.settle(harness.start(undefined, 180_000, "https://backend.onrender.com")), + { name: "BackendRequestTimeoutError" } + ); + assert.equal(browser.navigations.length, 1); + assert.equal(browser.mounted.size, 0); + assert.equal(harness.pendingTimers(), 0); +}); + +test("cancelling startup removes its document and all retry timers", async () => { + const browser = browserDocument(); + const harness = warmupHarness(() => response(429, "120"), browser); + const controller = new AbortController(); + harness.abortAfter(controller, 5_000); + await assert.rejects( + harness.settle(harness.start(controller.signal, 180_000, "https://backend.onrender.com")), + /Login cancelled/ + ); + assert.equal(browser.navigations.length, 1); + assert.equal(browser.mounted.size, 0); + assert.equal(harness.pendingTimers(), 0); +}); + +for (const baseUrl of [ + "/api/backend", + "http://backend.onrender.com", + "https://unrelated.example", + "https://backend.onrender.com.unrelated.example", + "https://user:password@backend.onrender.com", + "https://backend.onrender.com/path", + "https://backend.onrender.com/?query=private", +]) { + test(`no startup document is opened for ${baseUrl}`, async () => { + const browser = browserDocument(); + const harness = warmupHarness(() => healthy(), browser); + await harness.settle(harness.start(undefined, 180_000, baseUrl)); + assert.equal(browser.navigations.length, 0); + }); +} + test("ready backend proceeds immediately without credentials", async () => { const harness = warmupHarness([healthy()]); await harness.settle(harness.start());