66 lines
1.7 KiB
TypeScript
66 lines
1.7 KiB
TypeScript
import type { IncomingMessage } from 'http';
|
|
|
|
export interface ReadRequestBodyOptions {
|
|
maxBytes: number;
|
|
}
|
|
|
|
export class RequestBodyTooLargeError extends Error {
|
|
readonly maxBytes: number;
|
|
readonly receivedBytes: number;
|
|
|
|
constructor(maxBytes: number, receivedBytes: number) {
|
|
super(`Request body too large (${receivedBytes} bytes > ${maxBytes} bytes)`);
|
|
this.name = 'RequestBodyTooLargeError';
|
|
this.maxBytes = maxBytes;
|
|
this.receivedBytes = receivedBytes;
|
|
}
|
|
}
|
|
|
|
/** Read the full request body with an explicit max-size limit. */
|
|
export function readRequestBody(req: IncomingMessage, opts: ReadRequestBodyOptions): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const chunks: Buffer[] = [];
|
|
let totalBytes = 0;
|
|
let settled = false;
|
|
|
|
const cleanup = () => {
|
|
req.off('data', onData);
|
|
req.off('end', onEnd);
|
|
req.off('error', onError);
|
|
};
|
|
|
|
const fail = (err: Error) => {
|
|
if (settled) {return;}
|
|
settled = true;
|
|
cleanup();
|
|
reject(err);
|
|
};
|
|
|
|
const onData = (chunk: Buffer | string) => {
|
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
totalBytes += buf.length;
|
|
if (totalBytes > opts.maxBytes) {
|
|
if (typeof req.destroy === 'function') {
|
|
req.destroy();
|
|
}
|
|
fail(new RequestBodyTooLargeError(opts.maxBytes, totalBytes));
|
|
return;
|
|
}
|
|
chunks.push(buf);
|
|
};
|
|
|
|
const onEnd = () => {
|
|
if (settled) {return;}
|
|
settled = true;
|
|
cleanup();
|
|
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
};
|
|
|
|
const onError = (err: Error) => fail(err);
|
|
|
|
req.on('data', onData);
|
|
req.on('end', onEnd);
|
|
req.on('error', onError);
|
|
});
|
|
}
|