withastro/astro · error · ActionInputError
BAD_REQUEST
BAD_REQUEST
Error message
Failed to validate: ${JSON.stringify(issues, null, 2)} What it means
For accept:'form' actions with an input schema, Astro converts the FormData to an object (via formDataToObject) and validates it with zod. When validation fails, it throws an input error (code BAD_REQUEST) whose message embeds the full zod issues array as JSON, so every failing field and reason is visible in the error text.
Solutions
- Read the JSON issues array in the error message: each issue names the failing path and message — fix those fields first
- Add coercion for form fields: z.coerce.number(), z.coerce.boolean(), z.coerce.date() instead of plain z.number()
- On the caller side use the safe pattern (await actions.x(fd) then isInputError(result)) to render field errors instead of throwing
- Make optional fields optional in the schema (.optional()) when the form may omit them
Example fix
// before — schema assumes JSON types
const input = z.object({ qty: z.number().min(1) });
// form sends qty='3' (string) -> Failed to validate
// after
const input = z.object({ qty: z.coerce.number().min(1) }); Defensive patterns
Strategy: validation
Validate before calling
// validate against the shared schema before submitting the form
import { subscribeSchema } from './schema';
const parsed = subscribeSchema.safeParse(Object.fromEntries(new FormData(formEl)));
if (!parsed.success) {
renderFieldErrors(parsed.error.issues);
return;
}
await actions.subscribe(new FormData(formEl)); Type guard
import { isInputError } from 'astro:actions';
// after a safe (non-orThrow) call:
const result = await actions.subscribe(fd);
if (isInputError(result)) {
// result.error.issues: [{ path: ['email'], message: '...' }]
} Try / catch
try {
await actions.subscribe.orThrow(fd);
} catch (e) {
if (e instanceof ActionError && e.code === 'BAD_REQUEST' && e.message.includes('Failed to validate')) {
// parse the embedded issues JSON and show field errors
} else throw e;
} Prevention
- Always use z.coerce.* for number/boolean/date fields on accept:'form' actions
- Mark fields the form may omit as .optional() with defaults
- Validate client-side with the same shared schema for instant feedback
When it happens
Trigger: A required form field missing, a value failing its constraint (email format, min/max), or — most commonly — a numeric/boolean field sent as a string because raw form values are always strings and the schema lacks coercion (z.coerce.number()).
Common situations: HTML forms omitting optional-but-schema-required fields; forgetting z.coerce on number/boolean/date fields in form actions; whitespace or empty strings from unfilled inputs failing string constraints; client and server schemas drifting apart.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- BAD_REQUEST
- ActionCalledFromServerError
- ActionCalledFromServerError
- ActionNotFoundError
- ActionsReturnedInvalidDataError
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/49fc73ba189a24d2.
Report an issue: GitHub.
Appendix: source
Thrown at packages/astro/src/actions/runtime/server.ts:92
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);
const input =
baseSchema instanceof z.$ZodObject
? formDataToObject(unparsedInput, baseSchema)
: unparsedInput;
const parsed = await z.safeParseAsync(inputSchema, input);
return parsed;
}
function getJsonServerHandler<TOutput, TInputSchema extends z.$ZodType>(
handler: ActionHandler<TInputSchema, TOutput>,View on GitHub (pinned to 52e6c34790)