Fastify rate limit doesn't seem to work properly with async security prehandler hook and jest
### Prerequisites
- [x] I have written a descriptive issue title
- [x] I have searched existing issues to ensure the bug has not already been reported
### Fastify version
5.6.2
### Plugin version
10.3.0
### Node.js version
24.9.0
### Operating system
macOS
### Operating system version (i.e. 20.04, 11.3, 10)
15.6.1
### Description
Hey folks - I'm puzzled by how to set up an authentication preHandler hook when fastify rate limit is being used:
```
// prehandler-ratelimit.test.ts
import Fastify, {
FastifyInstance,
FastifyReply,
FastifyRequest,
} from 'fastify';
import rateLimit from '@fastify/rate-limit';
describe('preHandler + rateLimit', () => {
let app: FastifyInstance;
const handlerSpy = jest.fn();
beforeAll(async () => {
app = Fastify({ logger: false });
// Uncomment this and we fail
//await app.register(rateLimit, {
// max: 10,
// timeWindow: '1 minute',
//});
// Route under test
app.post(
'/test',
{
preHandler: async (req: FastifyRequest, reply: FastifyReply) => {
// Simulate auth failure
reply.code(401);
await reply.send({ error: 'unauthorized from preHandler' });
// Fastify should stop here - we should NOT enter the POST handler
},
},
async (req: FastifyRequest, reply: FastifyReply) => {
// This must NOT run if preHandler already sent the response
handlerSpy();
return { ok: true };
}
);
await app.ready();
});
afterAll(async () => {
await app.close();
});
it('does not call handler when preHandler sends 401', async () => {
const res = await app.inject({
method: 'POST',
url: '/test',
});
expect(res.statusCode).toBe(401);
expect(res.json()).toEqual({ error: 'unauthorized from preHandler' });
// Critical assertion: handler should never be invoked
expect(handlerSpy).not.toHaveBeenCalled();
});
});
```
When I run this code as is, it passes the test. When I uncomment out the fastify rate limit registration, I get this on the console:
```
bun test v1.3.0 (b0a6feca)
test/ratelimiter.integration.test.ts:
Debugger attached.
91 | this.on('timeout', callback)
92 | return this
93 | }
94 |
95 | Response.prototype.writeHead = function () {
96 | const result = http.ServerResponse.prototype.writeHead.apply(this, arguments)
^
error: Cannot writeHead headers after they are sent to the client
code: "ERR_HTTP_HEADERS_SENT"
at <anonymous> (node:_http_server:917:29)
at <anonymous> (/Users/ted/Projects/web/coin-server-be/node_modules/light-my-request/lib/response.js:96:58)
at <anonymous> (/Users/ted/Projects/web/coin-server-be/node_modules/fastify/lib/error-handler.js:45:19)
at fallbackErrorHandler (/Users/ted/Projects/web/coin-server-be/node_modules/fastify/lib/error-handler.js:142:3)
at handleError (/Users/ted/Projects/web/coin-server-be/node_modules/fastify/lib/error-handler.js:35:5)
at onErrorHook (/Users/ted/Projects/web/coin-server-be/node_modules/fastify/lib/reply.js:817:5)
at <anonymous> (/Users/ted/Projects/web/coin-server-be/node_modules/fastify/lib/reply.js:140:5)
at handleError (/Users/ted/Projects/web/coin-server-be/node_modules/fastify/lib/error-handler.js:78:11)
at onErrorHook (/Users/ted/Projects/web/coin-server-be/node_modules/fastify/lib/reply.js:817:5)
✗ preHandler + rateLimit > does not call handler when preHandler sends 401 [6.95ms]
0 pass
1 fail
3 expect() calls
Ran 1 test across 1 file. [93.00ms]
Debugger detached.
```
When I change the preHandler hook to be non-async it works perfectly. Am I just doing something crazy wrong here, or is this a defect?
### Link to code that reproduces the bug
https://github.com/tedmiddleton/fastify-rate-limiter-issue
### Expected Behavior
```
bun test v1.3.0 (b0a6feca)
test/ratelimiter.integration.test.ts:
Debugger attached.
✓ preHandler + rateLimit > does not call handler when preHandler sends 401 [6.54ms]
1 pass
0 fail
3 expect() calls
Ran 1 test across 1 file. [100.00ms]
Debugger detached.
```
关闭于 2025-12-13 2 条评论