From 29e699dc741b2993d445b17977caf96255690830 Mon Sep 17 00:00:00 2001 From: Marco Sadjadi Date: Thu, 28 May 2026 21:53:35 +0200 Subject: [PATCH] fix(preview/stream): emit CORS headers before flushHeaders() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @fastify/cors injects Access-Control-Allow-* in the onSend hook, but the SSE endpoint goes straight to reply.raw.flushHeaders() — onSend never runs, so the browser saw "No 'Access-Control-Allow-Origin' header" and blocked the fetch before any bytes flowed. Set Allow-Origin (reflecting the configured app origin), Allow-Credentials, and Vary: Origin manually right before the SSE content-type headers. Matches what the cors plugin would have emitted on a normal response. --- apps/api/src/routes/servers.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/apps/api/src/routes/servers.ts b/apps/api/src/routes/servers.ts index fad546c..b746de3 100644 --- a/apps/api/src/routes/servers.ts +++ b/apps/api/src/routes/servers.ts @@ -227,6 +227,19 @@ export async function serverRoutes(app: FastifyInstance): Promise { // so each chunk lands at the client immediately rather than after the // full response is built — critical for the keepalive-vs-CF-100s logic // to actually work. + // + // CORS note: @fastify/cors injects Access-Control-Allow-* in the onSend + // hook, which never runs once we go straight to reply.raw — that's why + // the browser saw "blocked by CORS policy". Set the headers manually + // here, mirroring what the plugin would have added: credentials:true + + // the configured app origin. The exact reflected Origin is fine because + // we already pinned it in the cors plugin registration. + const origin = req.headers.origin; + if (origin && origin === config.NEXT_PUBLIC_APP_URL) { + reply.raw.setHeader('Access-Control-Allow-Origin', origin); + reply.raw.setHeader('Access-Control-Allow-Credentials', 'true'); + reply.raw.setHeader('Vary', 'Origin'); + } reply.raw.setHeader('Content-Type', 'text/event-stream'); reply.raw.setHeader('Cache-Control', 'no-cache, no-transform'); reply.raw.setHeader('Connection', 'keep-alive');