This Fastify error occurs when your route handler or hook attempts to send a response multiple times for the same HTTP request. Fastify prevents duplicate responses, which would cause protocol violations and unpredictable client behavior.
The FST_ERR_REP_ALREADY_SENT error is Fastify's protection mechanism against sending multiple HTTP responses to the same request. Once reply.send() is called and the response is sent to the client, the reply object is marked as "already sent" and any subsequent attempts to send data will trigger this error. This error commonly occurs in async route handlers where execution continues after reply.send() has been called, or when multiple code paths inadvertently attempt to send responses. In Fastify, once a reply is sent, the request lifecycle is complete and no further modifications to the response are allowed. Understanding Fastify's request lifecycle is crucial: hooks like preHandler, handler, and onSend execute in sequence, and each can potentially send a reply. If any hook or handler sends a response without properly stopping execution (using return), subsequent code may attempt to send again, triggering this error.
The most common fix is to ensure you return immediately after calling reply.send() to prevent further code execution:
// ❌ WRONG - execution continues after send
fastify.get('/users/:id', async (request, reply) => {
const user = await findUser(request.params.id);
if (!user) {
reply.code(404).send({ error: 'User not found' });
// Code continues executing!
}
// This will execute even if user is null, causing double send
reply.send({ user });
});
// ✅ CORRECT - return prevents further execution
fastify.get('/users/:id', async (request, reply) => {
const user = await findUser(request.params.id);
if (!user) {
return reply.code(404).send({ error: 'User not found' });
}
return reply.send({ user });
});Always use return reply.send() to ensure the function exits immediately after sending the response.
Mixing async functions with the done() callback causes Fastify to process requests twice:
// ❌ WRONG - mixing async with done()
fastify.addHook('preHandler', async (request, reply, done) => {
try {
await authenticateUser(request);
done(); // This causes double execution!
} catch (error) {
done(error);
}
});
// ✅ CORRECT - use async without done()
fastify.addHook('preHandler', async (request, reply) => {
try {
await authenticateUser(request);
// No done() needed - just return or throw
} catch (error) {
throw error; // Fastify handles this automatically
}
});When using async/await, never use the done() callback. Fastify automatically handles promise resolution.
When calling reply.send() asynchronously outside the normal promise chain, you must return the reply object:
// ❌ WRONG - async send without returning reply
fastify.addHook('preHandler', async (request, reply) => {
setImmediate(() => {
reply.send({ message: 'Async response' });
});
// Fastify thinks handler completed without sending
});
// ✅ CORRECT - return reply to signal async send
fastify.addHook('preHandler', async (request, reply) => {
setImmediate(() => {
reply.send({ message: 'Async response' });
});
return reply; // Tells Fastify to wait for the async send
});This signals to Fastify that the reply will be sent asynchronously and prevents the framework from trying to send a default response.
Ensure error handlers don't attempt to send responses if one was already sent:
// ❌ WRONG - error handler doesn't check reply status
fastify.setErrorHandler(async (error, request, reply) => {
console.error(error);
reply.code(500).send({ error: error.message });
// Fails if route already sent a response
});
// ✅ CORRECT - check if reply was already sent
fastify.setErrorHandler(async (error, request, reply) => {
console.error(error);
// Check if reply was already sent
if (reply.sent) {
return; // Don't try to send again
}
reply.code(500).send({ error: error.message });
});The reply.sent property indicates whether a response has already been sent, preventing duplicate send attempts.
Check that only one hook or handler sends a response per request:
// ❌ WRONG - multiple hooks sending responses
fastify.addHook('onRequest', async (request, reply) => {
if (!request.headers.authorization) {
return reply.code(401).send({ error: 'Unauthorized' });
// This sends a response
}
});
fastify.get('/data', async (request, reply) => {
const data = await fetchData();
return reply.send({ data });
// This tries to send again if auth failed
});
// ✅ CORRECT - only one sends, others throw errors
fastify.addHook('onRequest', async (request, reply) => {
if (!request.headers.authorization) {
throw new Error('Unauthorized'); // Let error handler send response
}
});
fastify.get('/data', async (request, reply) => {
const data = await fetchData();
return reply.send({ data });
});Use hooks to validate and throw errors, not to send responses directly (unless terminating the request).
Enable Fastify's debug logging to see exactly where responses are being sent:
import fastify from 'fastify';
const app = fastify({
logger: {
level: 'debug', // Enable debug logging
prettyPrint: true
}
});
// Or use environment variable
// LOG_LEVEL=debug npm startDebug logs will show the complete request lifecycle, making it easier to identify where multiple reply.send() calls occur. Look for log entries showing "Reply sent" appearing twice for the same request ID.
Understanding Fastify's Reply Lifecycle:
Fastify tracks the reply state internally through the reply.sent flag. Once set to true, any subsequent send operation will fail. This is enforced at the framework level to comply with HTTP protocol requirements that each request receives exactly one response.
Common Plugin Conflicts:
Plugins like fastify-static, fastify-auth, and fastify-websocket can cause this error if not configured correctly. For example, fastify-static's sendFile in an onRequest hook may conflict with normal route handlers. Always check plugin documentation for proper lifecycle hook usage.
Performance Implications:
While the error prevents actual duplicate sends, the fact that your code is attempting double sends indicates a logic flaw that may impact performance. Each reply.send() call triggers serialization and validation - unnecessary duplicate calls waste CPU cycles even if they error out.
WebSocket Special Case:
With fastify-websocket, the "Reply already sent" error often occurs when mixing HTTP replies with WebSocket upgrades. Once a WebSocket connection is established, the HTTP reply is considered sent. Don't call reply.send() after accepting a WebSocket connection.
Testing Strategy:
Write unit tests that verify handlers only send one response per code path. Use Fastify's inject() method for testing and assert that reply.send() is called exactly once. Consider using test coverage tools to ensure all code paths are tested for proper reply handling.
TypeScript Type Safety:
TypeScript users can leverage Fastify's type system to catch some of these issues at compile time. The FastifyReply type includes methods that return the reply object, encouraging the return pattern that prevents double sends.
Error: Listener already called (once event already fired)
EventEmitter listener already called with once()
Error: EACCES: permission denied, open '/root/file.txt'
EACCES: permission denied
Error: Invalid encoding specified (stream encoding not supported)
How to fix Invalid encoding error in Node.js readable streams
Error: EINVAL: invalid argument, open
EINVAL: invalid argument, open
TypeError: readableLength must be a positive integer (stream config)
TypeError: readableLength must be a positive integer in Node.js streams