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
Before the browser sends an action call that is not FormData, the astro:actions client serializes the argument with JSON.stringify. If the value cannot be serialized — circular references, BigInt, symbols as keys — stringify throws, and the client wraps it as an ActionError with code BAD_REQUEST instead of posting an unusable body.
Solutions
- Convert BigInt to string before the call: String(value) or value.toString()
- Break circular references — send a plain DTO, not the live object graph
- For files or rich data, submit FormData instead (define the action with accept: 'form' if needed)
Example fix
// before
const id = 12345678901234567890n; // BigInt
await actions.saveProfile({ id }); // JSON.stringify throws
// after
await actions.saveProfile({ id: id.toString() }); Defensive patterns
Strategy: validation
Validate before calling
// cheap pre-flight serialization check before calling the action
function assertJsonSerializable(value: unknown): void {
JSON.stringify(value, (_k, v) => (typeof v === 'bigint' ? throwSerializationError('BigInt') : v));
}
function throwSerializationError(t: string): never { throw new Error(`Not JSON serializable: ${t}`); }
assertJsonSerializable(payload); // throws locally with a clear message
await actions.save(payload); Type guard
function isPlainJsonSerializable(v: unknown): boolean {
if (v === null || ['string', 'number', 'boolean'].includes(typeof v)) return true;
if (typeof v === 'bigint' || typeof v === 'function' || typeof v === 'symbol') return false;
if (Array.isArray(v)) return v.every(isPlainJsonSerializable);
if (v instanceof Object) {
const seen = new WeakSet();
// shallow circularity check plus recursion
return Object.values(v).every((x) => isPlainJsonSerializable(x));
}
return false;
} Try / catch
try {
await actions.save(payload);
} catch (e) {
if (e instanceof ActionError && e.code === 'BAD_REQUEST' && e.message.includes('serialize')) {
// sanitize payload: stringify BigInts, strip circular refs, then retry once
} else throw e;
} Prevention
- Convert BigInt to string at the boundary where it is produced
- Send DTOs built for transport, not live object graphs with back-references
- Use FormData (accept:'form') for anything containing files
When it happens
Trigger: Calling an action with a circular object (a DOM node graph, an object referencing itself), a BigInt, or class instances whose toJSON fails. Example: actions.save(bigintValue) or passing a parsed graph that contains parent back-references.
Common situations: Logging entities with circular references (user.friend = user); sending IDs as BigInt from a database layer; passing class instances (Map/Set are silently lost, BigInt/circular throw) from client components.
Understand the failure class
Background: BAD_REQUEST error code: request rejected as invalid (HTTP 400) - causes and fixes across libraries — this error's family across 8 libraries.
Related errors
- ActionsReturnedInvalidDataError
- BAD_REQUEST
- The passed value can't be serialized.
- ActionCalledFromServerError
- ActionCalledFromServerError
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/794dd8c148cc016e.
Report an issue: GitHub.
Appendix: 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 52e6c34790)