From 70727c904af5b561ef716c8233e3164d16dd1da4 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:21:17 -0500 Subject: [PATCH] fix: return no origin instead of throwing for disallowed CORS origins A request from an origin outside the whitelist gets HTTP 500, not a CORS denial. @tinyhttp/cors passes the origin() return value straight to res.setHeader and never wraps the call, so the thrown Error escapes the middleware and becomes a 500 for the whole request, including the OPTIONS preflight. That is wrong in three ways. It reports a client-side policy decision as a server fault, so it pages as an outage and reads like one in logs. It hides the real cause: the browser shows a generic 500 rather than a CORS message, and curl against the endpoint looks fine because a plain request sends no Origin at all. And enforcement does not belong on the server here anyway; omitting the header and letting the browser block the read is what the no-origin branch a few lines above already does. Returning "" for a disallowed origin makes the two paths consistent. It is also the only workable value: origin() feeds res.setHeader directly, so returning undefined throws ERR_HTTP_INVALID_HEADER_VALUE and returning false is not a valid header value. Credentials are enabled, so a wildcard is not an option and none is proposed. Nothing about which origins are allowed changes. Verified against @tinyhttp/cors 2.0.0, the pinned version, with a local harness running the current and proposed origin() side by side: current no Origin GET -> 200 acao="" allowed Origin GET -> 200 acao="https://meshtastic.org" disallowed Origin GET -> 500 acao=undefined disallowed Origin OPTIONS -> 500 acao=undefined proposed no Origin GET -> 200 acao="" allowed Origin GET -> 200 acao="https://meshtastic.org" disallowed Origin GET -> 200 acao="" disallowed Origin OPTIONS -> 204 acao="" The 500s match production today: api.meshtastic.org returns 500 with body "Origin not allowed by CORS" for https://client.meshtastic.org, and 200 with a reflected header for https://meshtastic.org and http://localhost:3000. A plain request with no Origin already receives an empty access-control-allow-origin in production, so the proposed value is behaviour this service is known to serve correctly. biome ci over src/ and scripts/ is unchanged at 0 errors and the same 2 pre-existing warnings, in files this commit does not touch. --- src/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index b0f9202..984ba91 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,7 +53,16 @@ app if (whitelist.indexOf(req.headers.origin) !== -1) { return req.headers.origin; } - throw new Error("Origin not allowed by CORS"); + + // Not allowed: return no origin rather than throwing. @tinyhttp/cors + // passes this return value straight to res.setHeader and does not catch, + // so throwing here escaped the middleware and turned every request from a + // non-whitelisted origin into a 500 -- including the OPTIONS preflight. + // An empty value is the same denial the no-origin branch above already + // returns: the browser sees no matching Access-Control-Allow-Origin and + // blocks the read, which is where that decision belongs. Credentials are + // enabled, so a wildcard is not an option. + return ""; }, }), )