withastro/astro · error · ActionError

UNSUPPORTED_MEDIA_TYPE

UNSUPPORTED_MEDIA_TYPE

Error message

This action only accepts FormData.

What it means

An action defined with accept: 'form' is wired to a form-specific server handler that requires its input to be a FormData instance (so it can run form parsing and the zod form path). If such an action receives any other value — a plain object, string, or URLSearchParams — the handler rejects it with ActionError code UNSUPPORTED_MEDIA_TYPE.

Solutions

  1. Build and pass FormData: const fd = new FormData(); fd.set('email', email); await actions.subscribe(fd)
  2. If calling from a real HTML form, pass the form element or its FormData (new FormData(formEl))
  3. If you actually want JSON input on this action, remove accept: 'form' from defineAction

Example fix

// before
await actions.subscribe({ email: 'a@b.c' }); // accept:'form' action

// after
const fd = new FormData();
fd.set('email', 'a@b.c');
await actions.subscribe(fd);
Defensive patterns

Strategy: type-guard

Validate before calling

// verify you are sending what an accept:'form' action expects
if (!(payload instanceof FormData)) {
  const fd = new FormData();
  for (const [k, v] of Object.entries(payload)) fd.set(k, String(v));
  payload = fd;
}
await actions.subscribe(payload);

Type guard

function isFormData(v: unknown): v is FormData {
  return typeof FormData !== 'undefined' && v instanceof FormData;
}

Try / catch

try {
  await actions.subscribe(fd);
} catch (e) {
  if (e instanceof ActionError && e.code === 'UNSUPPORTED_MEDIA_TYPE') {
    // convert input to FormData (or switch the action to JSON) and retry
  } else throw e;
}

Prevention

When it happens

Trigger: Calling an accept:'form' action from client code with a plain object: actions.subscribe({ email }) instead of FormData. Calling it from server code with a parsed object. Submitting programmatically built data without wrapping it in new FormData().

Common situations: Switching an action to accept:'form' for progressive enhancement but leaving JSON-style call sites; testing actions with object literals; reusing one call path for both json and form actions.

Related errors


AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18). Data as JSON: /api/errors/1eec47016b735560. Report an issue: GitHub.

Appendix: source

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

	Object.assign(safeServerHandler, {
		orThrow(this: ActionAPIContext, unparsedInput: unknown) {
			if (typeof this === 'function') {
				throw new AstroError(ActionCalledFromServerError);
			}
			return serverHandler(unparsedInput, this);
		},
	});

	return safeServerHandler as ActionClient<TOutput, TAccept, TInputSchema> & string;
}

function getFormServerHandler<TOutput, TInputSchema extends z.$ZodType>(
	handler: ActionHandler<TInputSchema, TOutput>,
	inputSchema?: TInputSchema,
) {
	return async (unparsedInput: unknown, context: ActionAPIContext): Promise<Awaited<TOutput>> => {
		if (!(unparsedInput instanceof FormData)) {
			throw new ActionError({
				code: 'UNSUPPORTED_MEDIA_TYPE',
				message: 'This action only accepts FormData.',
			});
		}

		if (!inputSchema) return await handler(unparsedInput, context);

		const parsed = await parseFormInput(inputSchema, unparsedInput);

		if (!parsed.success) {
			throw new ActionInputError(parsed.error.issues);
		}
		return await handler(parsed.data, context);
	};
}

async function parseFormInput(inputSchema: z.$ZodType, unparsedInput: FormData) {
	const baseSchema = unwrapBaseZ4ObjectSchema(inputSchema, unparsedInput);

View on GitHub (pinned to 52e6c34790)