diff --git a/netlify.toml b/netlify.toml index 29bc444..6665c11 100644 --- a/netlify.toml +++ b/netlify.toml @@ -40,6 +40,50 @@ to = "/.netlify/functions/qa-403" status = 200 +# QA-only: additional fixed-status endpoints handled by a single +# parameterized function (netlify/functions/qa-code.ts). The function +# inspects the public path it was hit at (req.url in v2 reflects the +# original /qa-NNN, not the internal /.netlify/functions/qa-code) and +# emits the matching status. Same rewrite-proxy mechanism as the QA +# endpoints above — status = 200 is the rewrite TYPE, not a forced +# response code, so Netlify preserves the function's real status code +# and headers (Location for 3xx, Content-Type for 4xx HTML). +# All MUST remain BEFORE the SPA catch-all below. +[[redirects]] + from = "/qa-302" + to = "/.netlify/functions/qa-code" + status = 200 + +[[redirects]] + from = "/qa-303" + to = "/.netlify/functions/qa-code" + status = 200 + +[[redirects]] + from = "/qa-307" + to = "/.netlify/functions/qa-code" + status = 200 + +[[redirects]] + from = "/qa-308" + to = "/.netlify/functions/qa-code" + status = 200 + +[[redirects]] + from = "/qa-401" + to = "/.netlify/functions/qa-code" + status = 200 + +[[redirects]] + from = "/qa-404" + to = "/.netlify/functions/qa-code" + status = 200 + +[[redirects]] + from = "/qa-429" + to = "/.netlify/functions/qa-code" + status = 200 + # Redirect all requests to /index.html for SPA routing [[redirects]] from = "/*" diff --git a/netlify/functions/qa-code.ts b/netlify/functions/qa-code.ts new file mode 100644 index 0000000..d7f2792 --- /dev/null +++ b/netlify/functions/qa-code.ts @@ -0,0 +1,90 @@ +// QA-only Netlify Function used to emit a fixed HTTP status code based on +// the public path the request came in on. One function, many rewrites in +// netlify.toml (one per code: /qa-302, /qa-303, /qa-307, /qa-308, /qa-401, +// /qa-404, /qa-429). Used to test how Prerender handles each status on a +// clean, page-looking URL. +// +// How status preservation works: +// - The status code AND headers (Location / Content-Type) are set by THIS +// function and passed through unchanged by Netlify's status=200 rewrite +// proxy in netlify.toml. The client sees a real 302/303/307/308/401/ +// 404/429 — not a 200 with error text, and Netlify does not auto-follow +// the 3xx redirects server-side. +// +// How the path is read: +// - In Netlify Functions v2, req.url reflects the URL the client actually +// requested (e.g. https://weirdanimals.life/qa-302), even when the +// function was reached via a rewrite-proxy. We parse the pathname and +// match against an explicit allow-list of codes. Unknown paths return +// a 404 so misconfiguration fails loud instead of silently emitting a +// wrong status. + +type RedirectConfig = { + kind: "redirect"; + status: number; +}; + +type HtmlConfig = { + kind: "html"; + status: number; + title: string; + heading: string; +}; + +type CodeConfig = RedirectConfig | HtmlConfig; + +const REDIRECT_LOCATION = "https://weirdanimals.life/"; + +const CODES: Record = { + "302": { kind: "redirect", status: 302 }, + "303": { kind: "redirect", status: 303 }, + "307": { kind: "redirect", status: 307 }, + "308": { kind: "redirect", status: 308 }, + "401": { kind: "html", status: 401, title: "401 - QA", heading: "Unauthorized" }, + "404": { kind: "html", status: 404, title: "404 - QA", heading: "Not Found" }, + "429": { kind: "html", status: 429, title: "429 - QA", heading: "Too Many Requests" }, +}; + +function renderHtml(title: string, heading: string): string { + return ` + + + + + ${title} + + +

${heading}

+ + +`; +} + +export default async (req: Request): Promise => { + const { pathname } = new URL(req.url); + const match = pathname.match(/^\/qa-(\d{3})\/?$/); + const code = match?.[1]; + const cfg = code ? CODES[code] : undefined; + + if (!cfg) { + return new Response( + `QA: no handler for path ${pathname}. Expected /qa- where is one of: ${Object.keys(CODES).join(", ")}.`, + { + status: 404, + headers: { "Content-Type": "text/plain; charset=utf-8" }, + }, + ); + } + + if (cfg.kind === "redirect") { + return new Response(null, { + status: cfg.status, + headers: { Location: REDIRECT_LOCATION }, + }); + } + + return new Response(renderHtml(cfg.title, cfg.heading), { + status: cfg.status, + headers: { "Content-Type": "text/html; charset=utf-8" }, + }); +};