A SvelteKit app whose SSR load functions fetch a cross-origin API will see Error: CORS error: No 'Access-Control-Allow-Origin' header is present on the requested resource in server-side error reporting. This is not a browser error and not a CORS misconfiguration, and the string exists nowhere in your source.
Where it comes from. @sveltejs/kit/src/runtime/server/page/load_data.js deliberately simulates CORS server-side "for consistency with client-side behaviour". For any fetch through the fetch passed to load where url.origin !== event.url.origin, it reads access-control-allow-origin off the response and throws:
const acao = response.headers.get('access-control-allow-origin');
if (!acao || (acao !== event.url.origin && acao !== '*')) {
throw new Error(`CORS error: ${acao ? 'Incorrect' : 'No'} 'Access-Control-Allow-Origin' header is present on the requested resource`);
}Why it fires intermittently rather than always. Server-to-server fetches send no Origin header. Starlette's CORSMiddleware (and most CORS middleware) only adds access-control-allow-origin when the request has an Origin. Verify with two curls:
curl -sI http://api.internal/healthcheck # no ACAO
curl -sI -H 'Origin: https://app.example' http://api.internal/health # ACAO presentSo whether it throws depends on which responses happen to carry the header. In practice it fires whenever an infrastructure error page answers instead of your app: a PaaS proxy 502, a CDN error page, a gateway timeout. Those never pass through your CORS middleware, so the honest 502/504 gets replaced by a misleading "CORS error" and any status-based error handling you wrote never runs.
Identifying it in an error tracker. Two tells, both decisive: the event's runtime tag is node, and the transaction name uses the HTTP verb (GET /some/path) rather than the client route id. A browser-only explanation for a runtime: node event is always wrong. We watched this issue for twelve days assuming it was a client CORS fault.
Fix. In handleFetch, stamp the header on responses from your own API before returning them. CORS is a browser concept; a first-party server-to-server call has no security boundary to protect here, and browsers still run their own CORS check against the API's real headers (SvelteKit serialises load data, not response headers).
function allow_ssr_cors(response: Response, origin: string): Response {
if (response.headers.get('access-control-allow-origin') === origin) return response;
const headers = new Headers(response.headers);
headers.set('access-control-allow-origin', origin);
// Null-body statuses (204/205/304) arrive with a null body, as the ctor requires.
return new Response(response.body, { status: response.status, statusText: response.statusText, headers });
}
export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
const response = await fetch(request);
return is_api ? allow_ssr_cors(response, event.url.origin) : response;
};Apply it to every return path in handleFetch, including retry paths. Passing response.body through preserves streaming instead of buffering.
General lesson: when an error string appears nowhere in your source, grep node_modules before concluding it came from the browser. Frameworks synthesise browser-flavoured errors on the server.