Request body errors
UnsupportedMediaTypeError and MalformedBodyError: what req.json() and req.formData() throw for a body sent the wrong way, and how to catch them.
import {
UnsupportedMediaTypeError,
isUnsupportedMediaTypeError,
MalformedBodyError,
isMalformedBodyError,
} from '@gio.js/core';You rarely need to catch them: uncaught, they become a 415 or a 400 response instead of a 500. Catch them when one handler accepts more than one encoding, or to word the error yourself.
Reference
What throws what
| Call | Body | Throws | Uncaught response |
|---|---|---|---|
req.json() | Content-Type is not application/json or application/*+json (or is missing) | UnsupportedMediaTypeError | 415 |
req.formData() | Content-Type is not application/x-www-form-urlencoded or multipart/form-data | UnsupportedMediaTypeError | 415 |
req.formData() | Declared as a form but does not parse (a broken multipart body, a missing boundary) | MalformedBodyError | 400 |
req.json() | Declared as JSON but empty, not UTF-8 text, or not valid JSON | MalformedBodyError | 400 |
The 415 and 400 bodies are JSON, and nothing is logged - they are client errors:
{"error":"Unsupported Media Type","message":"request body must be sent as application/json (got text/plain)"}This applies to route.ts handlers and page actions alike. Media type parameters (; charset=utf-8) are ignored.
UnsupportedMediaTypeError
| Field | Type | Default | Description |
|---|---|---|---|
status | 415 | - | The status it answers with. |
contentType | string | undefined | - | The Content-Type the request declared. |
message | string | - | What was expected and what arrived. |
name | string | - | 'UnsupportedMediaTypeError' |
Why JSON is not parsed from any body: an HTML form on another site can post text/plain or a form encoding without a CORS preflight, while a real application/json request from another origin needs one. A handler that parsed whatever arrived would accept a forged "JSON" request the preflight would otherwise have stopped.
isUnsupportedMediaTypeError
isUnsupportedMediaTypeError(value: unknown): value is UnsupportedMediaTypeErrorisUnsupportedMediaTypeError() returns true for an UnsupportedMediaTypeError from any copy of @gio.js/core. Use it rather than instanceof: route files load in their own module namespace, so the class your handler imports may not be the one that threw.
MalformedBodyError
| Field | Type | Default | Description |
|---|---|---|---|
status | 400 | - | The status it answers with. |
message | string | - | Starts with request body is not valid and the media type (JSON for req.json()), followed by the reason: the parser's message, the body is empty or it is not UTF-8 text. |
name | string | - | 'MalformedBodyError' |
isMalformedBodyError
isMalformedBodyError(value: unknown): value is MalformedBodyErrorisMalformedBodyError() returns true for a MalformedBodyError from any copy of @gio.js/core.
Examples
Accept JSON and a plain form
import { isUnsupportedMediaTypeError, type GioRequest } from '@gio.js/core';
/** Accepts JSON from fetch() and a plain HTML form post alike. */
export async function POST(req: GioRequest) {
let text: string;
try {
text = req.json<{ text: string }>().text;
} catch (err) {
if (!isUnsupportedMediaTypeError(err)) throw err;
const form = await req.formData(); // still 415 for anything but a form
text = String(form.get('text') ?? '');
}
return Response.json({ saved: text }, { status: 201 });
}JSON and form posts both answer 201; a text/plain body answers 415.
Your own error message
import { isMalformedBodyError, type GioRequest } from '@gio.js/core';
export async function PUT(req: GioRequest) {
try {
const form = await req.formData();
return { fields: [...form.keys()] };
} catch (err) {
if (isMalformedBodyError(err)) {
return Response.json({ error: 'That upload was cut off - try again.' }, { status: err.status });
}
throw err;
}
}Invalid JSON as a 400
Uncaught, invalid JSON already answers 400 {"error":"Bad Request","message":"request body is not valid JSON: ..."}. Catch it to send your own body:
import { isMalformedBodyError, type GioRequest } from '@gio.js/core';
export function PATCH(req: GioRequest) {
let patch: unknown;
try {
patch = req.json();
} catch (err) {
if (isMalformedBodyError(err)) return Response.json({ error: 'Invalid JSON' }, { status: 400 });
throw err; // UnsupportedMediaTypeError stays a 415
}
return { applied: patch };
}Good to know
- Invalid JSON is a 400, not a
SyntaxError.req.json()throwsMalformedBodyErrorfor JSON that does not parse, a request without a body, and a body that is not UTF-8 text (which the server forwards base64-encoded). The parser'sSyntaxError(theUnexpected token ...orUnexpected end of JSON input) is not rethrown, so acatchthat testserr instanceof SyntaxErrorno longer matches: testisMalformedBodyError(err). - Bodies over
[server] max_body_bytes(2 MiB by default) never reach your code: the server answers413.max_body_bytes = 0lifts that limit, leaving only the worker's 64 MiB message cap (about 48 MiB of binary body), and logs a startup warning. req.bodyalways holds the raw body (base64 whenreq.bodyBase64is true), whatever its type.- An empty form post parses to an empty
FormData, not aMalformedBodyError.
Related
Version history
| Version | Changes |
|---|---|
v0.1.0-beta.8 | Introduced. req.json() parses only bodies declared as JSON; req.formData() is new, with MalformedBodyError for bodies that do not parse. req.json() throws MalformedBodyError (a 400) too, for an empty, non-UTF-8 or invalid JSON body, where it threw a SyntaxError or a plain Error (a 500). |