From 66f1d9aebb07ebde308d454507baaac266ec3938 Mon Sep 17 00:00:00 2001 From: Local Dev Date: Thu, 22 Oct 2026 00:26:13 +0300 Subject: [PATCH] gateway: strip query string before building the S3 key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A URL like /messenger/?v=1 landed in the Sia lookup as the key "bns/hermes/messenger/?v=1", which 404s. The SPA fallback then served the apex /index.html — the site's home — but under a anchored at /messenger/, so every relative asset the home expects (assets/favicon.svg, assets/shared.css, assets/mark.svg) resolved to /messenger/assets/... and 404d. The visible effect: hitting a real subpath with any query-string cache-buster rendered the home unstyled, with no icon and no header. Fix is one derived variable: compute pathOnly = reqPath.split("?")[0] once, use it for every S3 key and fallback-check. reqPath itself is preserved for the "ip" and "p" cases where the full URL (including query) is forwarded to the upstream. The fix lands only in the s3 case because the ip/p cases pass the full URL to the backend by design. --- src/gateway/public-gateway.mjs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/gateway/public-gateway.mjs b/src/gateway/public-gateway.mjs index fe1b6d2..9145e74 100644 --- a/src/gateway/public-gateway.mjs +++ b/src/gateway/public-gateway.mjs @@ -940,13 +940,20 @@ async function serve(res, name, reqPath, baseHref, req = null) { case "s3": { const sub = subFolderFor(name, entry.name, manifest); let base = r.s3; if (base.endsWith("/") && sub) base += sub + "/"; + // Strip the query string before building the S3 key, otherwise a + // simple cache-buster like `?v=1` lands in the key as + // `bns/foo/page/?v=1` → 404 → SPA fallback serves the wrong page + // (the apex home with a base href pointing at /page/, which breaks + // every relative asset the home expects). The query only ever + // matters to the browser; the Sia lookup should be path-only. + const pathOnly = reqPath.split("?")[0]; // Auto-index directory paths. S3 has no dir listing, so `/tools/` // would 404 (NoSuchKey) without this — matches how nginx handles // `try_files … /index.html` and how Ariadne's bnsd.js rewrites the // same URLs client-side. - const rel = reqPath === "/" ? "index.html" - : reqPath.endsWith("/") ? reqPath.slice(1) + "index.html" - : reqPath.slice(1); + const rel = pathOnly === "/" ? "index.html" + : pathOnly.endsWith("/") ? pathOnly.slice(1) + "index.html" + : pathOnly.slice(1); const key = base.endsWith("/") ? base + rel : base; let up = await aws.fetch(`${S3_ENDPOINT}/${key}`); // Fallback chain for missing extensionless paths (order matters): @@ -958,14 +965,14 @@ async function serve(res, name, reqPath, baseHref, req = null) { // Requests with a file extension (missing .png / .js / .json) are // NOT rescued — those should never be routed by a client-side // router and swallowing them would hide real bugs. - if (up.status === 404 && !/\.[a-z0-9]+$/i.test(reqPath) && reqPath !== "/") { + if (up.status === 404 && !/\.[a-z0-9]+$/i.test(pathOnly) && pathOnly !== "/") { const cleanBase = base.endsWith("/") ? base : base + "/"; - const cleanPath = reqPath.slice(1).replace(/\/+$/, ""); + const cleanPath = pathOnly.slice(1).replace(/\/+$/, ""); // 1) Extensionless-URL support: /tld -> tld.html. Lets sites // publish clean URLs without keeping mirror files around. // Base injection stays anchored at the directory of reqPath, // which is correct because tld.html is a flat file. - if (!reqPath.endsWith("/")) { + if (!pathOnly.endsWith("/")) { const htmlKey = cleanBase + cleanPath + ".html"; const htmlHit = await aws.fetch(`${S3_ENDPOINT}/${htmlKey}`); if (htmlHit.ok) { up = htmlHit; } @@ -976,11 +983,11 @@ async function serve(res, name, reqPath, baseHref, req = null) { // relative asset/href inside the served page (e.g. // ./assets/favicon.svg on /theseus/index.html) resolves to // the WRONG directory. nginx behaves the same way. - if (up.status === 404 && !reqPath.endsWith("/")) { + if (up.status === 404 && !pathOnly.endsWith("/")) { const dirKey = cleanBase + cleanPath + "/index.html"; const dir = await aws.fetch(`${S3_ENDPOINT}/${dirKey}`); if (dir.ok) { - res.writeHead(302, { location: `${MOUNT}/${name}${reqPath}/` }); + res.writeHead(302, { location: `${MOUNT}/${name}${pathOnly}/` }); return res.end(); } }