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(); } }