79 lines
3.0 KiB
TypeScript
79 lines
3.0 KiB
TypeScript
import { error } from '@sveltejs/kit';
|
|
import { MAX_ARTIFACT_BYTES } from '../artifact-limits.js';
|
|
|
|
export const JSON_BODY_LIMIT = 1024 * 1024;
|
|
// Leave room for form fields and multipart framing around a maximum-size file.
|
|
export const ARTIFACT_FORM_LIMIT = MAX_ARTIFACT_BYTES + 1024 * 1024;
|
|
export const BETA_REVISION_FORM_LIMIT = 2 * MAX_ARTIFACT_BYTES + 1024 * 1024;
|
|
|
|
/** Enforce the limit before parsing, including absent or understated lengths. */
|
|
export async function boundedBody(request: Request, maxBytes: number) {
|
|
if (Number(request.headers.get('content-length')) > maxBytes) {
|
|
void request.body?.cancel().catch(() => {});
|
|
error(413, 'Request is too large.');
|
|
}
|
|
const reader = request.body?.getReader();
|
|
if (!reader) return Buffer.alloc(0);
|
|
const chunks: Buffer[] = [];
|
|
let block = Buffer.alloc(0);
|
|
let used = 0;
|
|
let received = 0;
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
received += value.byteLength;
|
|
if (received > maxBytes) {
|
|
// Cancellation may wait for another branch of a cloned request.
|
|
void reader.cancel().catch(() => {});
|
|
error(413, 'Request is too large.');
|
|
}
|
|
// Coalesce tiny chunks so bookkeeping cannot outgrow the byte limit.
|
|
let offset = 0;
|
|
while (offset < value.byteLength) {
|
|
if (used === block.length) {
|
|
block = Buffer.allocUnsafe(Math.min(maxBytes, 64 * 1024));
|
|
chunks.push(block);
|
|
used = 0;
|
|
}
|
|
const length = Math.min(value.byteLength - offset, block.length - used);
|
|
block.set(value.subarray(offset, offset + length), used);
|
|
used += length;
|
|
offset += length;
|
|
}
|
|
}
|
|
} finally {
|
|
reader.releaseLock();
|
|
}
|
|
return Buffer.concat(chunks, received);
|
|
}
|
|
|
|
// Match Request.json()'s result type; each route retains its input validation.
|
|
export async function jsonBody(
|
|
request: Request,
|
|
maxBytes = JSON_BODY_LIMIT
|
|
): ReturnType<Request['json']> {
|
|
return JSON.parse((await boundedBody(request, maxBytes)).toString('utf8'));
|
|
}
|
|
|
|
export async function artifactFormBody(
|
|
request: Request,
|
|
maxBytes = ARTIFACT_FORM_LIMIT
|
|
): Promise<FormData> {
|
|
const body = await boundedBody(request, maxBytes);
|
|
let form: FormData;
|
|
try {
|
|
form = await new Response(body, {
|
|
headers: { 'Content-Type': request.headers.get('content-type') ?? '' }
|
|
}).formData();
|
|
} catch {
|
|
error(400, 'Request body must be valid form data.');
|
|
}
|
|
// Validate every part before any handler copies file bytes or writes versions.
|
|
for (const value of form.values()) {
|
|
const size = typeof value === 'string' ? Buffer.byteLength(value, 'utf8') : value.size;
|
|
if (size > MAX_ARTIFACT_BYTES) error(413, 'Artifacts must not exceed 32 MiB.');
|
|
}
|
|
return form;
|
|
}
|