fix(gateway): enforce request body size limits
This commit is contained in:
@@ -438,3 +438,68 @@ describe('GatewayServer HTTP auth', () => {
|
||||
expect(res.headers.get('content-type')).toBe('text/html');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GatewayServer request body limits', () => {
|
||||
const BODY_PORT = 18896;
|
||||
let bodyLimitServer: GatewayServer;
|
||||
const gmailHandler = {
|
||||
handlePushNotification: vi.fn(async () => {}),
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!LISTEN_ALLOWED) {
|
||||
return;
|
||||
}
|
||||
bodyLimitServer = new GatewayServer({
|
||||
port: BODY_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'],
|
||||
gmailHandler: gmailHandler as unknown as GatewayServerConfig['gmailHandler'],
|
||||
maxRequestBodyBytes: 64,
|
||||
uiDir: resolve(import.meta.dirname, 'ui'),
|
||||
});
|
||||
await bodyLimitServer.start();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (!LISTEN_ALLOWED) {
|
||||
return;
|
||||
}
|
||||
await bodyLimitServer.stop();
|
||||
});
|
||||
|
||||
it('accepts gmail push body under limit', async () => {
|
||||
if (!LISTEN_ALLOWED) {
|
||||
return;
|
||||
}
|
||||
gmailHandler.handlePushNotification.mockClear();
|
||||
|
||||
const body = JSON.stringify({ message: { data: 'abc' } });
|
||||
const res = await fetch(`http://127.0.0.1:${BODY_PORT}/gmail/push`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
expect(res.status).toBe(200);
|
||||
expect(gmailHandler.handlePushNotification).toHaveBeenCalledWith('abc');
|
||||
});
|
||||
|
||||
it('rejects gmail push body over limit with 413', async () => {
|
||||
if (!LISTEN_ALLOWED) {
|
||||
return;
|
||||
}
|
||||
gmailHandler.handlePushNotification.mockClear();
|
||||
|
||||
const body = JSON.stringify({ message: { data: 'x'.repeat(2048) } });
|
||||
const res = await fetch(`http://127.0.0.1:${BODY_PORT}/gmail/push`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
expect(res.status).toBe(413);
|
||||
expect(gmailHandler.handlePushNotification).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
+14
-9
@@ -41,6 +41,7 @@ import type { CommandRegistry } from '../commands/index.js';
|
||||
import type { ComponentRegistry } from '../intents/index.js';
|
||||
import type { RoutingPolicy } from '../routing/index.js';
|
||||
import type { ChannelRegistry } from '../channels/index.js';
|
||||
import { RequestBodyTooLargeError, readRequestBody } from '../utils/httpBody.js';
|
||||
|
||||
export interface GatewayServerConfig {
|
||||
port: number;
|
||||
@@ -67,6 +68,8 @@ export interface GatewayServerConfig {
|
||||
gmailHandler?: GmailWatcher;
|
||||
/** Optional callback to retrieve per-session token usage data for the dashboard. */
|
||||
getTokenUsage?: () => TokenUsageEntry[];
|
||||
/** Maximum allowed request body size for inbound HTTP POST bodies. */
|
||||
maxRequestBodyBytes?: number;
|
||||
/** Optional pairing manager for DM pairing code management via gateway. */
|
||||
pairingManager?: PairingManager;
|
||||
memoryStore?: MemoryStore;
|
||||
@@ -76,6 +79,7 @@ export interface GatewayServerConfig {
|
||||
}
|
||||
|
||||
export class GatewayServer {
|
||||
private static readonly DEFAULT_MAX_REQUEST_BODY_BYTES = 1_048_576; // 1 MiB
|
||||
private wss: WebSocketServer | null = null;
|
||||
private httpServer: HttpServer | null = null;
|
||||
private router: Router;
|
||||
@@ -348,9 +352,14 @@ export class GatewayServer {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ ok: true }));
|
||||
} catch (err) {
|
||||
console.error('Gmail push handler error:', err instanceof Error ? err.message : err);
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid request' }));
|
||||
if (err instanceof RequestBodyTooLargeError) {
|
||||
res.writeHead(413, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Payload too large' }));
|
||||
} else {
|
||||
console.error('Gmail push handler error:', err instanceof Error ? err.message : err);
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Invalid request' }));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -444,11 +453,7 @@ export class GatewayServer {
|
||||
|
||||
/** Read the full request body as a string. */
|
||||
private readRequestBody(req: IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks: Buffer[] = [];
|
||||
req.on('data', (chunk: Buffer) => chunks.push(chunk));
|
||||
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
||||
req.on('error', reject);
|
||||
});
|
||||
const maxBytes = this.config.maxRequestBodyBytes ?? GatewayServer.DEFAULT_MAX_REQUEST_BODY_BYTES;
|
||||
return readRequestBody(req, { maxBytes });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user