~ 6 min read
Avoid Fastify's reply.raw and reply.hijack Despite Being A Powerful HTTP Streams Tool
As web developers, we often encounter situations where we need fine-grained control over the HTTP request and response process. It might be about handling large file uploads, implementing real-time features, or building proxy servers. Either way, having the ability to manipulate data streams in real-time can be a game-changer and a powerful tool.
Fastify, the blazing-fast web framework for Node.js, exposes a powerful set of HTTP response APIs via reply.raw
and reply.hijack()
that allows us to take control of the HTTP stream. As they say though, with great power comes great responsibility.
In this article, we will explore what reply.hijack()
is and why avoiding raw HTTP replies via reply.raw
should be a last resort.
Understanding reply.raw
Fastify’s reply.raw
grants developers access to the low-level API of Node.js’s underlying HTTP subsystem.
reply.raw
is a method provided by Fastify that exposes the underlying Node.js http.ServerResponse
object. This grants developers direct access to the low-level HTTP interface, allowing them to perform advanced operations and customization that go beyond the traditional abstractions offered by the framework.
Other reasons developers might turn to Fastify’s reply.raw
could be to gain the ability to manipulate headers, handle data streams directly, and perform other low-level operations. This level of control is particularly valuable in scenarios where precise optimizations, customization, or integration with other Node.js libraries are necessary.
Raw HTTP replies use-case: Server-Sent Events
One practical use-case where reply.raw shines is in implementing Server-Sent Events (SSE). SSE enables servers to push real-time updates to clients over a single HTTP connection. With reply.raw
, we can effortlessly stream events to clients in a controlled and optimized manner.
Let’s consider an example:
fastify.get('/stream', async (request, reply) => {
const { res } = reply.raw;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.write('data: Welcome!\n\n');
// Send a new event every second
setInterval(() => {
res.write(`data: Event ${Date.now()}\n\n`);
}, 1000);
});
What is reply.hijack()?
Let’s dive deeper into raw HTTP replies in Fastify web applications and meet reply.hijack()
.
reply.hijack()
is a method provided by Fastify that allows you to “hijack” the underlying HTTP stream and take control of its read and write operations. When you call this method, Fastify hands over the socket and allows you to interact with it directly. This means you can bypass the framework’s usual request-response flow and handle the stream at a low level.
Specifically, when you call reply.hijack()
then Fastify knows you want to complete sending the HTTP response by yourself using raw replies.
The Fastify documentation shows a code example for aborting Fastify’s own reply.send()
and instead controlling the response stream yourself:
app.get('/', (req, reply) => {
reply.hijack()
reply.raw.end('hello world')
return Promise.resolve('this will be skipped')
})
Important to point out - in the above code, Fastify won’t be appending any HTTP headers.
Use-case: Proxy servers
One example use-case might be to use reply.hijack()
to build proxy servers, where you need to forward requests to another server while modifying or intercepting the data being sent or received.
By hijacking the stream, you can act as a middleware, inspecting and manipulating the incoming and outgoing data.
Let’s consider a simple example:
fastify.get('/proxy', async (request, reply) => {
reply.hijack();
const { socket } = reply.raw;
// Forward the request to the target server
const targetSocket = net.connect(8000, 'target-server.com');
socket.pipe(targetSocket).pipe(socket);
});
In this example, when a GET request is made to /proxy
, we establish a connection to a target server. By piping the sockets together, we create a transparent proxy, forwarding the data from the client to the target server and vice versa. With reply.hijack()
, we have full control over the proxy behavior and can apply custom logic to modify or analyze the data being exchanged.
Why should you avoid Fastify’s reply.raw
and reply.hijack()
While this set of APIs makes a powerful tool to control HTTP responses, it also hides an important high-level concept when working within a framework.
If it wasn’t clear until now - when you resort to raw HTTP responses then you skip and bypass the entire HTTP response logic by Fastify, including onRequest
hooks and other response lifecycle operations.
This seems logical at first but when you take a step back and consider the entire web concerns for any reasonable web application then you soon realize where hijacking Fastify’s response hooks for raw HTTP responses misses out:
-
CORS: if you enabled CORS via something like
@fastify/cors
then no CORS related headers will be added to the response and you’d need to manually do that in the route you hijacked the response. -
Authentication: if you enabled some sort of authentication plugin such as
@fastify/jwt
which often adds anonRequest
server handler to verify a JWT token such asawait request.jwtVerify();
then you’ve completely bypassed that too.
Fastify raw HTTP replies with reply.hijack and reply.raw: Pitfalls and Considerations
While reply.hijack()
offers immense power and flexibility, it’s essential to understand its potential pitfalls and considerations:
-
Increased complexity: by using
reply.hijack()
, you’re essentially bypassing Fastify’s built-in request-response flow. This can introduce complexity, as you’re responsible for handling low-level operations such as data parsing, error handling, and managing the socket’s lifecycle. It’s crucial to have a solid understanding of network protocols and the underlying stream mechanics to handle these complexities effectively. -
Risk of blocking: since
reply.hijack()
allows you to control the HTTP stream in real-time, it’s essential to avoid blocking operations. Long-running or computationally expensive tasks can lead to blocking the event loop (think: RegEx for example), affecting the performance and responsiveness of your application.
A better alternative to raw requests with Fastify’s stream responses
If your raw HTTP responses use-case is about modifying response on-the-fly, then you can achieve that by working with streams. And hey, after all, HTTP is streamable data protocol.
And with that, consider the following example:
import Fastify from "fastify";
import { Readable } from "stream";
const server = Fastify();
server.post("/", async (request, reply) => {
const readableStream = new Readable();
readableStream._read = () => {};
reply.header("Content-Type", "application/json; charset=utf-8");
reply.send(readableStream);
// Simulate asynchronous processing of the request
setTimeout(() => {
// Push the desired data down the stream
readableStream.push(JSON.stringify({ message: "Hello, world!" }));
}, 1000);
// Nothing else to do after 5 seconds so we close the stream
setTimeout(() => {
// Push the desired data down the stream
readableStream.push(null); // End the stream when the client closes the connection
}, 5000);
return reply;
});
server.listen(3000, (err) => {
if (err) {
console.error("Error starting server:", err);
process.exit(1);
}
console.log("Server listening on port 3000");
});
In the above we use a readable stream that we pipe to Fastify via reply.send(readableStream)
. Since Fastify natively supports streams, it allows us to continue piping data to the stream until we want to denote that we’re finished (via readableStream.push(null)
) at which point the HTTP response ends.
The best part? Any onRequest
hooks or other logic that is part of Fastify’s plugin and hooks systems is maintained, including HTTP response headers such as CORS.