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

  1. Read the JSON issues array in the error message: each issue names the failing path and message — fix those fields first
  2. Add coercion for form fields: z.coerce.number(), z.coerce.boolean(), z.coerce.date() instead of plain z.number()
  3. On the caller side use the safe pattern (await actions.x(fd) then isInputError(result)) to render field errors instead of throwing
  4. 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

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


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)