skip to content

Search

Fixing CORS Issues in Fastify When Using reply.raw for SSE

2 min read

Using reply.raw in Fastify can bypass CORS and other middleware. Here's how it affects SSE and how to avoid it.

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 onSend or onResponse

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-Type headers
  • 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.