Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/utils/http/readLimitedRequestBody.server.ts
T

95 lines
3.1 KiB
TypeScript

export class RequestBodyLimitError extends Error {
constructor(readonly limitBytes: number) {
super(`Request body exceeds the ${limitBytes}-byte route limit.`);
this.name = "RequestBodyLimitError";
}
}
export async function readLimitedRequestBytes(request: Request, maxBytes: number): Promise<Uint8Array> {
assertValidRequestBodyLimit(maxBytes);
await rejectOversizedDeclaredBody(request, maxBytes);
if (!request.body) return new Uint8Array();
const reader = request.body.getReader();
const chunks: Uint8Array[] = [];
let totalBytes = 0;
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
totalBytes += chunk.value.byteLength;
if (totalBytes > maxBytes) {
await reader.cancel().catch(() => undefined);
throw new RequestBodyLimitError(maxBytes);
}
chunks.push(chunk.value);
}
const body = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
body.set(chunk, offset);
offset += chunk.byteLength;
}
return body;
}
/**
* Parse multipart or URL-encoded form data while enforcing the bytes actually
* consumed by the parser. `Content-Length` remains a fast rejection path, but
* missing or understated headers cannot bypass the limit.
*/
export async function readLimitedRequestFormData(request: Request, maxBytes: number): Promise<FormData> {
assertValidRequestBodyLimit(maxBytes);
await rejectOversizedDeclaredBody(request, maxBytes);
if (!request.body) return request.formData();
const limitedBody = createLimitedRequestBodyStream(request.body, maxBytes);
const contentType = request.headers.get("content-type");
return new Response(limitedBody, {
headers: contentType ? { "content-type": contentType } : undefined,
}).formData();
}
function assertValidRequestBodyLimit(maxBytes: number): void {
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
throw new TypeError("Request body limit must be a positive safe integer.");
}
}
async function rejectOversizedDeclaredBody(request: Request, maxBytes: number): Promise<void> {
const contentLength = request.headers.get("content-length");
if (contentLength && /^\d+$/.test(contentLength) && Number(contentLength) > maxBytes) {
await request.body?.cancel().catch(() => undefined);
throw new RequestBodyLimitError(maxBytes);
}
}
function createLimitedRequestBodyStream(body: ReadableStream<Uint8Array>, maxBytes: number): ReadableStream<Uint8Array> {
const reader = body.getReader();
let totalBytes = 0;
return new ReadableStream<Uint8Array>({
async pull(controller) {
try {
const chunk = await reader.read();
if (chunk.done) {
controller.close();
return;
}
totalBytes += chunk.value.byteLength;
if (totalBytes > maxBytes) {
await reader.cancel().catch(() => undefined);
controller.error(new RequestBodyLimitError(maxBytes));
return;
}
controller.enqueue(chunk.value);
} catch (error) {
controller.error(error);
}
},
cancel(reason) {
return reader.cancel(reason);
},
});
}