withastro/astro · error · TypeError

Unsupported content type

Error message

Unsupported content type

What it means

parseRequestBody only recognizes form content types (application/x-www-form-urlencoded, multipart/form-data) and application/json. Any other Content-Type falls through to throw new TypeError('Unsupported content type').

Source

Thrown at packages/astro/src/actions/runtime/server.ts:302

		if (hasContentType(contentType, ['application/json'])) {
			if (contentLength === 0) return undefined;
			if (!hasContentLength) {
				const body = await readBodyWithLimit(request.clone(), bodySizeLimit);
				if (body.byteLength === 0) return undefined;
				return JSON.parse(new TextDecoder().decode(body));
			}
			return await request.clone().json();
		}
	} catch (e) {
		if (e instanceof BodySizeLimitError) {
			throw new ActionError({
				code: 'CONTENT_TOO_LARGE',
				message: `Request body exceeds ${bodySizeLimit} bytes`,
			});
		}
		throw e;
	}
	throw new TypeError('Unsupported content type');
}

export const ACTION_API_CONTEXT_SYMBOL = Symbol.for('astro.actionAPIContext');

const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];

function hasContentType(contentType: string, expected: string[]) {
	// Split off parameters like charset or boundary
	// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type#content-type_in_html_forms
	const type = contentType.split(';')[0].toLowerCase();

	return expected.some((t) => type === t);
}

function isActionAPIContext(ctx: ActionAPIContext): boolean {
	const symbol = Reflect.get(ctx, ACTION_API_CONTEXT_SYMBOL);
	return symbol === true;
}

View on GitHub (pinned to d081033d5f)

Solutions

  1. Send application/json for JSON actions (let the client proxy set it).
  2. For form posts, ensure a proper form Content-Type with a valid boundary.
  3. Verify the request Content-Type header on the client before sending.

Example fix

// before - manual fetch with wrong header
await fetch(actionUrl, { method: 'POST', headers: { 'Content-Type': 'text/plain' }, body: JSON.stringify(data) });
// after
await fetch(actionUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
Defensive patterns

Strategy: validation

Validate before calling

// Only send recognized content types to action requests.
const ALLOWED = new Set(['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data']);
const ct = headers.get('Content-Type')?.split(';')[0].trim().toLowerCase();
if (!ct || !ALLOWED.has(ct)) throw new Error(`Unsupported Content-Type: ${ct}`);

Type guard

function isSupportedContentType(ct: string): boolean {
  const base = ct.split(';')[0].trim().toLowerCase();
  return ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data'].includes(base);
}

Try / catch

try {
  await actions.myAction(payload);
} catch (e) {
  if (e instanceof TypeError && /Unsupported content type/i.test(e.message)) {
    // set a proper Content-Type and retry
  } else throw e;
}

Prevention

When it happens

Trigger: An action request whose Content-Type is text/plain, application/xml, a malformed multipart without a valid boundary, or any unrecognized type.

Common situations: A custom fetch that forgets to set Content-Type; a client sending text/plain; a proxy rewriting the header; a corrupted multipart boundary.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/384b8d65a602bc7a. Report an issue: GitHub.