withastro/astro · error · AstroError
`getActionState()` must be called with a form request.
Error message
`getActionState()` must be called with a form request.
What it means
React's useActionState submits the action as a form post, and getActionState() reads the state object out of that form body. Before parsing, it checks the request Content-Type and requires application/x-www-form-urlencoded or multipart/form-data; anything else (usually a JSON submission, i.e. an action without accept: 'form') throws.
Solutions
- Define the action with accept: 'form' so submissions arrive as form data
- Submit through a real <form> / useActionState so the browser posts urlencoded or multipart data
- If the action must stay JSON, do not call getActionState; carry state through the action input instead
Example fix
// before
const action = defineAction({ handler: async ({ request }) => { const s = await getActionState({ request }); return s; } });
// after
const action = defineAction({ accept: 'form', handler: async ({ request }) => { const s = await getActionState({ request }); return s; } }); Defensive patterns
Strategy: validation
Validate before calling
const ct = request.headers.get('Content-Type') ?? '';
const isForm = ct.startsWith('application/x-www-form-urlencoded') || ct.startsWith('multipart/form-data');
if (!isForm) throw new Response('Expected a form submission', { status: 415 });
const state = await getActionState({ request }); Type guard
function isFormRequest(contentType: string): boolean {
const base = contentType.split(';')[0].trim();
return base === 'application/x-www-form-urlencoded' || base === 'multipart/form-data';
} Try / catch
try {
state = await getActionState({ request });
} catch (err) {
if (String(err?.message).includes('form request')) {
state = null; // JSON submission path; no React state expected
} else throw err;
} Prevention
- Set accept: 'form' on every action that uses useActionState/getActionState
- Submit such actions only through real forms or useActionState
- Add a component test that submits the form and asserts the state round-trip
When it happens
Trigger: Calling getActionState({ request }) inside an action defined without accept: 'form' (Astro defaults to JSON and the client posts JSON); invoking the action via fetch with a JSON body; calling getActionState from an API route that received a non-form request.
Common situations: Adding useActionState to an existing JSON action without flipping accept; progressive-enhancement setups where some submissions are programmatic JSON; forgetting that the accept option controls the wire format.
Related errors
- `getActionState()` could not find a state object.
- [@astrojs/react] Importing `getContainerRenderer` from…
- BAD_REQUEST
- INTERNAL_SERVER_ERROR
- More than one JSX renderer is enabled. This will lead to…
AI-assisted analysis of withastro/astro@52e6c34790 (2026-08-18).
Data as JSON: /api/errors/c5a4275b86d4e22e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/integrations/react/src/actions.ts:50
// React calls `.bind()` internally to pass the initial state value.
// Calling `.bind()` seems to remove our `$$FORM_ACTION` metadata,
// so we need to define our *own* `.bind()` method to preserve that metadata.
Object.defineProperty(callback, 'bind', {
value: (...args: Parameters<typeof callback>) =>
injectStateIntoFormActionData(callback, ...args),
});
return callback;
}
/**
* Retrieve the state object from your action handler when using `useActionState()`.
* To ensure this state is retrievable, use the {@linkcode withState} helper.
*/
export async function getActionState<T>({ request }: { request: Request }): Promise<T> {
const contentType = request.headers.get('Content-Type');
if (!contentType || !isFormRequest(contentType)) {
throw new AstroError(
'`getActionState()` must be called with a form request.',
"Ensure your action uses the `accept: 'form'` option.",
);
}
const formData = await request.clone().formData();
const state = formData.get('_astroActionState')?.toString();
if (!state) {
throw new AstroError(
'`getActionState()` could not find a state object.',
'Ensure your action was passed to `useActionState()` with the `withState()` wrapper.',
);
}
return JSON.parse(state) as T;
}
const formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];
function isFormRequest(contentType: string) {View on GitHub (pinned to 52e6c34790)