feat(gateway): add websocket ingress rate limiting
This commit is contained in:
@@ -503,3 +503,77 @@ describe('GatewayServer request body limits', () => {
|
||||
expect(gmailHandler.handlePushNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GatewayServer WebSocket ingress rate limiting', () => {
|
||||
const RATE_PORT = 18895;
|
||||
let rateServer: GatewayServer;
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!LISTEN_ALLOWED) {
|
||||
return;
|
||||
}
|
||||
rateServer = new GatewayServer({
|
||||
port: RATE_PORT,
|
||||
sessionManager: mockSessionManager as unknown as GatewayServerConfig['sessionManager'],
|
||||
modelClient: mockModelClient,
|
||||
systemPrompt: 'Test prompt',
|
||||
toolRegistry: mockToolRegistry as unknown as GatewayServerConfig['toolRegistry'],
|
||||
toolExecutor: mockToolExecutor as unknown as GatewayServerConfig['toolExecutor'],
|
||||
uiDir: resolve(import.meta.dirname, 'ui'),
|
||||
wsRateLimit: {
|
||||
enabled: true,
|
||||
capacity: 2,
|
||||
refillPerSec: 1,
|
||||
maxViolations: 3,
|
||||
violationWindowMs: 10_000,
|
||||
},
|
||||
});
|
||||
await rateServer.start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!LISTEN_ALLOWED) {
|
||||
return;
|
||||
}
|
||||
await rateServer.stop();
|
||||
});
|
||||
|
||||
it('throttles bursts and closes repeated offenders', async () => {
|
||||
if (!LISTEN_ALLOWED) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ws = await new Promise<WebSocket>((resolve, reject) => {
|
||||
const c = new WebSocket(`ws://127.0.0.1:${RATE_PORT}`);
|
||||
c.on('open', () => resolve(c));
|
||||
c.on('error', reject);
|
||||
});
|
||||
|
||||
try {
|
||||
const first = await sendAndReceive(ws, { id: 1, method: 'system.health' });
|
||||
expect((first as GatewayResponse).id).toBe(1);
|
||||
|
||||
const second = await sendAndReceive(ws, { id: 2, method: 'system.health' });
|
||||
expect((second as GatewayResponse).id).toBe(2);
|
||||
|
||||
const third = await sendAndReceive(ws, { id: 3, method: 'system.health' });
|
||||
const rateErr = third as GatewayError;
|
||||
expect(rateErr.error.code).toBe(ErrorCode.InternalError);
|
||||
expect(rateErr.error.message).toContain('Rate limit exceeded');
|
||||
|
||||
// Trigger additional violations; server should close on max violation threshold.
|
||||
ws.send(JSON.stringify({ id: 4, method: 'system.health' }));
|
||||
ws.send(JSON.stringify({ id: 5, method: 'system.health' }));
|
||||
|
||||
const close = await new Promise<{ code: number; reason: string }>((resolve) => {
|
||||
ws.on('close', (code, reason) => resolve({ code, reason: reason.toString() }));
|
||||
});
|
||||
expect(close.code).toBe(4008);
|
||||
expect(close.reason).toContain('Rate limit exceeded');
|
||||
} finally {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user