fix(preview/stream): emit CORS headers before flushHeaders()
All checks were successful
Deploy to Production / deploy (push) Successful in 1m22s

@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.
This commit is contained in:
Marco Sadjadi 2026-05-28 21:53:35 +02:00
parent 31bfeed9dd
commit 29e699dc74

View File

@ -227,6 +227,19 @@ export async function serverRoutes(app: FastifyInstance): Promise<void> {
// 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');