mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-10-01 23:58:36 -05:00
Migrate /art/new to SendouForm
This commit is contained in:
@@ -10,6 +10,13 @@ export type ParseResult<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; fieldErrors: Record<string, string> };
|
||||
|
||||
/**
|
||||
* Ceiling for a request body, in bytes. Sized to fit a form with a couple of `image()` fields (each
|
||||
* capped at ~3M base64 characters) plus its other fields. Forms that legitimately submit more (e.g.
|
||||
* art) pass their own `maxBodyBytes`.
|
||||
*/
|
||||
const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
|
||||
|
||||
/**
|
||||
* Maps a {@link z.ZodError} to field-level errors keyed by form field name
|
||||
* (e.g. `members[0].userId`), keeping the first error per field.
|
||||
@@ -34,14 +41,14 @@ function fieldErrorsFromZodError(error: z.ZodError): Record<string, string> {
|
||||
export async function parseFormData<T extends z.ZodTypeAny>({
|
||||
request,
|
||||
schema,
|
||||
maxBodyBytes = DEFAULT_MAX_BODY_BYTES,
|
||||
}: {
|
||||
request: Request;
|
||||
schema: T;
|
||||
/** Overrides {@link DEFAULT_MAX_BODY_BYTES} for forms that legitimately submit a bigger body. */
|
||||
maxBodyBytes?: number;
|
||||
}): Promise<ParseResult<z.infer<T>>> {
|
||||
const data =
|
||||
request.headers.get("Content-Type") === "application/json"
|
||||
? await request.json()
|
||||
: formDataToObject(await request.formData());
|
||||
const data = await requestBodyToObject(request, maxBodyBytes);
|
||||
|
||||
const result = await schema.safeParseAsync(data);
|
||||
|
||||
@@ -116,3 +123,52 @@ function imageFields(
|
||||
|
||||
return [...fields].map(([key, autoValidate]) => ({ key, autoValidate }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the request body into the plain object the schema parses, refusing anything over
|
||||
* `maxBytes`. `Content-Length` is checked up front so an oversized body is rejected before a byte
|
||||
* of it is read.
|
||||
*/
|
||||
async function requestBodyToObject(request: Request, maxBytes: number) {
|
||||
if (Number(request.headers.get("Content-Length")) > maxBytes) {
|
||||
throw payloadTooLarge();
|
||||
}
|
||||
|
||||
if (request.headers.get("Content-Type") === "application/json") {
|
||||
return JSON.parse(await readBodyText(request, maxBytes));
|
||||
}
|
||||
|
||||
return formDataToObject(await request.formData());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the body as text, aborting the stream as soon as `maxBytes` is exceeded. The running total
|
||||
* is enforced (rather than trusting `Content-Length`) so a chunked body that omits or understates
|
||||
* the header can't be buffered in full either.
|
||||
*/
|
||||
async function readBodyText(request: Request, maxBytes: number) {
|
||||
const reader = request.body?.getReader();
|
||||
if (!reader) return "";
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let bytesRead = 0;
|
||||
let text = "";
|
||||
|
||||
let chunk = await reader.read();
|
||||
while (!chunk.done) {
|
||||
bytesRead += chunk.value.byteLength;
|
||||
if (bytesRead > maxBytes) {
|
||||
await reader.cancel();
|
||||
throw payloadTooLarge();
|
||||
}
|
||||
|
||||
text += decoder.decode(chunk.value, { stream: true });
|
||||
chunk = await reader.read();
|
||||
}
|
||||
|
||||
return text + decoder.decode();
|
||||
}
|
||||
|
||||
function payloadTooLarge() {
|
||||
return new Response(null, { status: 413 });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user