GioJSdocs
On this page

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.

ts
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

CallBodyThrowsUncaught response
req.json()Content-Type is not application/json or application/*+json (or is missing)UnsupportedMediaTypeError415
req.formData()Content-Type is not application/x-www-form-urlencoded or multipart/form-dataUnsupportedMediaTypeError415
req.formData()Declared as a form but does not parse (a broken multipart body, a missing boundary)MalformedBodyError400
req.json()Declared as JSON but empty, not UTF-8 text, or not valid JSONMalformedBodyError400

The 415 and 400 bodies are JSON, and nothing is logged - they are client errors:

json
{"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

FieldTypeDefaultDescription
status415-The status it answers with.
contentTypestring | undefined-The Content-Type the request declared.
messagestring-What was expected and what arrived.
namestring-'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

ts
isUnsupportedMediaTypeError(value: unknown): value is UnsupportedMediaTypeError

isUnsupportedMediaTypeError() 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

FieldTypeDefaultDescription
status400-The status it answers with.
messagestring-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.
namestring-'MalformedBodyError'

isMalformedBodyError

ts
isMalformedBodyError(value: unknown): value is MalformedBodyError

isMalformedBodyError() returns true for a MalformedBodyError from any copy of @gio.js/core.

Examples

Accept JSON and a plain form

app/api/notes/route.ts
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

app/api/upload/route.ts
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:

app/api/settings/route.ts
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() throws MalformedBodyError for 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's SyntaxError (the Unexpected token ... or Unexpected end of JSON input) is not rethrown, so a catch that tests err instanceof SyntaxError no longer matches: test isMalformedBodyError(err).
  • Bodies over [server] max_body_bytes (2 MiB by default) never reach your code: the server answers 413. max_body_bytes = 0 lifts that limit, leaving only the worker's 64 MiB message cap (about 48 MiB of binary body), and logs a startup warning.
  • req.body always holds the raw body (base64 when req.bodyBase64 is true), whatever its type.
  • An empty form post parses to an empty FormData, not a MalformedBodyError.

Version history

VersionChanges
v0.1.0-beta.8Introduced. 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).