While implementing Server-Sent Events (SSE) using
Fastify, I ran into a subtle but important gotcha. Using
reply.raw in Fastify gives you direct access to Node.js’s native
http.ServerResponse object, but it bypasses middleware like CORS, compression,
and lifecycle hooks like OnSend.
The Scenario
I was setting up an SSE endpoint, and naturally reached for the low-level
reply.raw to manually manage the connection and keep it open:
fastify.get("/events", (req, reply) => {
reply.raw.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});
reply.raw.write(`data: hello\n\n`);
});This seemed fine at first, but when I tested it in the browser, the frontend was throwing CORS errors, and the SSE connection wasn’t working properly.
It turned out that since I was using reply.raw, Fastify’s CORS middleware was
not applying the Access-Control-Allow-Origin header. That header is required
for the browser to accept the connection.
What You’re Bypassing
By using reply.raw, you’re skipping:
- Fastify’s
reply.send(), which handles serialisation - Any plugin-added headers or logic (e.g. compression, CORS, etc.)
- Lifecycle hooks like
onSendoronResponse
In this case, the missing CORS header was the issue. Without it, the browser rejected the connection.
The Safer Alternative
If you don’t need low-level control, stick with the built-in reply API:
fastify.get("/standard-endpoint", (req, reply) => {
reply.code(200).send({
hello: "world",
});
});This ensures:
- Proper
Content-Typeheaders - JSON serialisation
- Compatibility with plugins like CORS
If you do use reply.raw, be aware that you’re responsible for setting
everything yourself, including security-related headers like CORS.
Takeaway
Using reply.raw gives you full control over the response, but it opts you out
of built-in behaviours like header injection, plugin support, and lifecycle
hooks. Be careful when using it in browser-facing endpoints like SSE, where
things like CORS headers are critical.