diff --git a/src/gateway/public-gateway.mjs b/src/gateway/public-gateway.mjs
index 23817d9..8d8f59c 100644
--- a/src/gateway/public-gateway.mjs
+++ b/src/gateway/public-gateway.mjs
@@ -77,6 +77,19 @@ const nx = (h) => `
// apex, current priority stands (h → s3 → ip → u). This does not break
// Sia-per-subdomain owners: if they omit `ip` from the parent, the fallback
// still tries `s3` first as before.
+function ipFetch(ip, reqPath, hostHeader) {
+ return new Promise((resolve, reject) => {
+ const req = http.request({ host: ip, port: 80, path: reqPath, method: "GET", headers: { Host: hostHeader } }, (r) => {
+ const chunks = [];
+ r.on("data", (c) => chunks.push(c));
+ r.on("end", () => resolve({ status: r.statusCode, headers: r.headers, body: Buffer.concat(chunks) }));
+ });
+ req.on("error", reject);
+ req.setTimeout(15000, () => req.destroy(new Error("upstream timeout")));
+ req.end();
+ });
+}
+
async function serve(res, name, reqPath, baseHref) {
let entry;
try { entry = await resolve(name); } catch (e) { res.writeHead(502); return res.end("resolve error: " + e.message); }
@@ -102,11 +115,15 @@ async function serve(res, name, reqPath, baseHref) {
res.writeHead(up.status, { "content-type": ct }); return res.end(buf);
}
case "ip": {
- const up = await fetch(`http://${r.ip}${reqPath}`, { headers: { host: name } });
- const buf = Buffer.from(await up.arrayBuffer());
- let ct = up.headers.get("content-type") || guess(reqPath);
- if (ct.includes("text/html")) { res.writeHead(up.status, { "content-type": ct }); return res.end(injectBase(buf.toString("utf8"), baseHref)); }
- res.writeHead(up.status, { "content-type": ct }); return res.end(buf);
+ // http.request instead of fetch(): WHATWG fetch treats Host as a
+ // forbidden header, so undici silently drops { headers: { host: name } }
+ // and sends Host: . nginx then falls to default_server and 404s.
+ // The whole point of an ip record is to reach a server_name matching the
+ // BCNR name, so Host must be right.
+ const up = await ipFetch(r.ip, reqPath, name);
+ let ct = up.headers["content-type"] || guess(reqPath);
+ if (ct.includes("text/html")) { res.writeHead(up.status, { "content-type": ct }); return res.end(injectBase(up.body.toString("utf8"), baseHref)); }
+ res.writeHead(up.status, { "content-type": ct }); return res.end(up.body);
}
case "u":
res.writeHead(302, { location: r.u }); return res.end();