withastro/astro · error · ActionError
BAD_REQUEST
BAD_REQUEST
Error message
Failed to serialize request body to JSON. Full error: ${(e as Error).message} What it means
The client action proxy serializes each call argument with JSON.stringify before sending it as the request body. If the argument contains values JSON cannot represent (circular references, functions, Symbols, BigInt, DOM nodes), serialization throws and is wrapped as an ActionError with code BAD_REQUEST (400).
Source
Thrown at packages/astro/src/actions/runtime/entrypoints/client.ts:53
export const getActionPath = createGetActionPath({
baseUrl: import.meta.env.BASE_URL,
shouldAppendTrailingSlash,
});
export const actions = createActionsProxy({
handleAction: async (param, path) => {
const headers = new Headers();
headers.set('Accept', 'application/json');
// Apply adapter-specific headers for internal fetches
for (const [key, value] of Object.entries(internalFetchHeaders)) {
headers.set(key, value);
}
let body = param;
if (!(body instanceof FormData)) {
try {
body = JSON.stringify(param);
} catch (e) {
throw new ActionError({
code: 'BAD_REQUEST',
message: `Failed to serialize request body to JSON. Full error: ${(e as Error).message}`,
});
}
if (body) {
headers.set('Content-Type', 'application/json');
} else {
headers.set('Content-Length', '0');
}
}
const rawResult = await fetch(
getActionPathFromString({
baseUrl: import.meta.env.BASE_URL,
shouldAppendTrailingSlash,
path: getActionQueryString(path),
}),
{
method: 'POST',View on GitHub (pinned to d081033d5f)
Solutions
- Extract only plain serializable primitives (strings/numbers/booleans/arrays/plain objects/Dates) before calling the action.
- If you need to send files or form fields, define the action with accept: 'form' and pass a FormData instance instead.
- Strip functions, Symbols, and circular references; convert BigInt to string/number; convert class instances to plain objects.
Example fix
// before
<form onSubmit={(e) => actions.create({ event: e, extra: () => 1 })} />
// after
<form onSubmit={(e) => {
const data = new FormData(e.currentTarget);
// call a form-accept action, or:
actions.create({ title: data.get('title'), count: Number(data.get('count')) });
}} /> Defensive patterns
Strategy: validation
Validate before calling
// Validate serializability before calling an action from the client.
function isJsonSerializable(value: unknown): boolean {
const seen = new WeakSet();
try {
JSON.stringify(value, (_k, v) => {
if (typeof v === 'object' && v !== null) {
if (seen.has(v)) return undefined; // circular -> we still proceed, JSON.stringify throws on cycles
seen.add(v);
}
if (typeof v === 'function' || typeof v === 'symbol' || typeof v === 'bigint') return undefined;
return v;
});
return true;
} catch {
return false;
}
}
if (!isJsonSerializable(payload)) throw new Error('Action payload is not JSON-serializable'); Type guard
// Reject obviously non-serializable shapes before sending.
function isPlainSerializable(v: unknown): boolean {
if (v === null || v === undefined) return true;
const t = typeof v;
if (t === 'string' || t === 'number' || t === 'boolean') return true;
if (t === 'function' || t === 'symbol' || t === 'bigint') return false;
if (t !== 'object') return false;
if (v instanceof Date || v instanceof URL) return true;
if (Array.isArray(v)) return v.every(isPlainSerializable);
return Object.values(v as Record<string, unknown>).every(isPlainSerializable);
} Try / catch
try {
await actions.myAction(payload);
} catch (e) {
if (e instanceof ActionError && e.code === 'BAD_REQUEST' && /serialize/i.test(e.message)) {
showUserError('Please fill the form with valid values.');
} else throw e;
} Prevention
- Send only plain objects/arrays/primitives/Dates/URLs to JSON actions.
- Use FormData with an accept: 'form' action for files and form fields.
- Strip DOM events down to their fields before calling.
When it happens
Trigger: Calling actions.myAction(value) from a client component where value is a DOM event, a React/Vue synthetic event, a class instance with circular refs, a function, a Symbol, an unsupported BigInt, or a DOM element.
Common situations: Passing the whole submit event object to an action; passing a component instance or ref; sending a dayjs/Moment object (has methods) instead of a serializable shape; sending FormData to a JSON action instead of a form-accept action.
Related errors
- ActionsReturnedInvalidDataError
- The passed value can't be serialized.
- [astro:actions] `defineAction()` unexpectedly used on the cl
- [astro:actions] `getActionContext()` unexpectedly used on th
- ActionCalledFromServerError
AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12).
Data as JSON: /api/errors/794dd8c148cc016e.
Report an issue: GitHub.