This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user