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

Thrown by @astrojs/react's `getActionState()` when the incoming request's `Content-Type` is missing or not a form content type (`application/x-www-form-urlencoded` or `multipart/form-data`). State retrieval relies on parsing `FormData`, which only exists for form posts.

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 d081033d5f)

Solutions

  1. Define the action with `accept: 'form'` so Astro routes form posts to it.
  2. Ensure the React client submits via a `<form action={withState(action)}>` or `useActionState`, which always posts form data.
  3. Guard `getActionState` behind a content-type check if the action handles multiple accept types.

Example fix

// before
export const action = defineAction({ handler: async ({ request }) => {
  const state = await getActionState({ request }); // throws for JSON
}});
// after
export const action = defineAction({
  accept: 'form',
  handler: async ({ request }) => {
    const state = await getActionState({ request });
  },
});
Defensive patterns

Strategy: type-guard

Validate before calling

function isFormRequest(request: Request): boolean {
  const ct = request.headers.get('Content-Type') ?? '';
  const base = ct.split(';')[0].toLowerCase();
  return base === 'application/x-www-form-urlencoded' || base === 'multipart/form-data';
}
if (!isFormRequest(request)) { /* respond 415 or skip getActionState */ }

Type guard

function isFormContentType(request: Request): request is Request & { clone(): Promise<Request & { formData(): Promise<FormData> }> } {
  const ct = (request.headers.get('Content-Type') ?? '').split(';')[0].toLowerCase();
  return ct === 'application/x-www-form-urlencoded' || ct === 'multipart/form-data';
}

Try / catch

try { await getActionState({ request }); } catch (e) { if (/must be called with a form request/) respondUnsupportedMedia(); else throw e; }

Prevention

When it happens

Trigger: Calling `getActionState({ request })` inside an action whose `accept` option is `'json'` or omitted while the client sends JSON. A fetch to the action endpoint with `Content-Type: application/json`. A GET request (no body/content-type).

Common situations: Forgetting to set `accept: 'form'` on an action that calls `getActionState`. Mixing JSON-based actions with React `useActionState` flow. A non-React client calling the action endpoint.

Related errors


AI-assisted analysis of withastro/astro@d081033d5f (2026-08-12). Data as JSON: /api/errors/c5a4275b86d4e22e. Report an issue: GitHub.