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
- Build and pass FormData: const fd = new FormData(); fd.set('email', email); await actions.subscribe(fd)
- If calling from a real HTML form, pass the form element or its FormData (new FormData(formEl))
- 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
- Match call-site input type to the action's accept option: form -> FormData, default -> plain object
- Build FormData with new FormData(formElement) from real forms to keep types aligned
- Document the accept mode in the action name or JSDoc when a project mixes both styles
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
- ActionCalledFromServerError
- ActionCalledFromServerError
- ActionNotFoundError
- ActionsReturnedInvalidDataError
- [astro:actions] `defineAction()` unexpectedly used on the…
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)