Hmbown/CodeWhale · error · FormBodyError

expected application/x-www-form-urlencoded

Error message

expected application/x-www-form-urlencoded

What it means

FormBodyError with HTTP 415, thrown by readBoundedUrlEncodedForm() when the request's media type (content-type before any ';' parameters, lowercased) is not exactly application/x-www-form-urlencoded. The helper accepts only that encoding so it can enforce its byte limit while buffering.

Source

Thrown at web/lib/bounded-form.ts:17

export class FormBodyError extends Error {
  constructor(
    readonly status: 400 | 413 | 415,
    message: string
  ) {
    super(message);
    this.name = "FormBodyError";
  }
}

export async function readBoundedUrlEncodedForm(
  request: Request,
  maxBytes: number
): Promise<URLSearchParams> {
  const mediaType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
  if (mediaType !== "application/x-www-form-urlencoded") {
    throw new FormBodyError(415, "expected application/x-www-form-urlencoded");
  }

  const rawLength = request.headers.get("content-length");
  if (rawLength !== null) {
    if (!/^\d+$/.test(rawLength)) throw new FormBodyError(400, "invalid Content-Length");
    if (Number(rawLength) > maxBytes) throw new FormBodyError(413, "payload too large");
  }

  if (!request.body) return new URLSearchParams();

  const reader = request.body.getReader();
  const chunks: Uint8Array[] = [];
  let total = 0;
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    total += value.byteLength;
    if (total > maxBytes) {

View on GitHub (pinned to 8880682c63)

Solutions

  1. Send the body as URLSearchParams (it sets the header automatically) or as urlencoded text with content-type: application/x-www-form-urlencoded
  2. If the request must be JSON or multipart, switch the route to a reader for that encoding
  3. Remove content-type overrides added by HTTP interceptors

Example fix

// before
await fetch('/submit', { method: 'POST', body: JSON.stringify({ q: 'hi' }) });

// after - URLSearchParams sets application/x-www-form-urlencoded automatically
await fetch('/submit', { method: 'POST', body: new URLSearchParams({ q: 'hi' }) });
Defensive patterns

Strategy: validation

Validate before calling

const mediaType = (request.headers.get('content-type') || '').split(';', 1)[0].trim().toLowerCase();
if (mediaType !== 'application/x-www-form-urlencoded') {
  return new Response('expected application/x-www-form-urlencoded', { status: 415 });
}
const params = await readBoundedUrlEncodedForm(request, maxBytes);

Try / catch

try {
  const params = await readBoundedUrlEncodedForm(request, MAX_BYTES);
} catch (err) {
  if (err instanceof FormBodyError) {
    return new Response(err.message, { status: err.status, headers: { 'content-type': 'text/plain' } });
  }
  throw err;
}

Prevention

When it happens

Trigger: POSTing JSON to a route that reads its body with readBoundedUrlEncodedForm(); an HTML form with enctype="multipart/form-data"; a fetch whose body is FormData (browser sends multipart) or a plain string (text/plain).

Common situations: A front-end migrates a form to fetch with a JSON body but the Workers route still expects urlencoded; curl --json or an explicit application/json header against a form route; file-upload forms reusing a urlencoded-only endpoint.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@8880682c63 (2026-08-16). Data as JSON: /api/errors/c8de89cfa50949e6. Report an issue: GitHub.