gateway: strip query string before building the S3 key

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 <base href>
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.
This commit is contained in:
Local Dev 2026-10-22 00:26:13 +03:00
parent 23757e7a03
commit 66f1d9aebb

View file

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