{"record":{"id":"c5a4275b86d4e22e","repo":"withastro/astro","slug":"getactionstate-must-be-called-with-a-form-requ","errorCode":null,"errorMessage":"`getActionState()` must be called with a form request.","messagePattern":"`getActionState\\(\\)` must be called with a form request\\.","errorType":"exception","errorClass":"AstroError","httpStatus":null,"severity":"error","filePath":"packages/integrations/react/src/actions.ts","lineNumber":50,"sourceCode":"\n\t// React calls `.bind()` internally to pass the initial state value.\n\t// Calling `.bind()` seems to remove our `$$FORM_ACTION` metadata,\n\t// so we need to define our *own* `.bind()` method to preserve that metadata.\n\tObject.defineProperty(callback, 'bind', {\n\t\tvalue: (...args: Parameters<typeof callback>) =>\n\t\t\tinjectStateIntoFormActionData(callback, ...args),\n\t});\n\treturn callback;\n}\n\n/**\n * Retrieve the state object from your action handler when using `useActionState()`.\n * To ensure this state is retrievable, use the {@linkcode withState} helper.\n */\nexport async function getActionState<T>({ request }: { request: Request }): Promise<T> {\n\tconst contentType = request.headers.get('Content-Type');\n\tif (!contentType || !isFormRequest(contentType)) {\n\t\tthrow new AstroError(\n\t\t\t'`getActionState()` must be called with a form request.',\n\t\t\t\"Ensure your action uses the `accept: 'form'` option.\",\n\t\t);\n\t}\n\tconst formData = await request.clone().formData();\n\tconst state = formData.get('_astroActionState')?.toString();\n\tif (!state) {\n\t\tthrow new AstroError(\n\t\t\t'`getActionState()` could not find a state object.',\n\t\t\t'Ensure your action was passed to `useActionState()` with the `withState()` wrapper.',\n\t\t);\n\t}\n\treturn JSON.parse(state) as T;\n}\n\nconst formContentTypes = ['application/x-www-form-urlencoded', 'multipart/form-data'];\n\nfunction isFormRequest(contentType: string) {","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/withastro/astro/blob/d081033d5fe8e8a68c4bbbad4af9d2deb9c74bca/packages/integrations/react/src/actions.ts#L32-L68","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Define the action with `accept: 'form'` so Astro routes form posts to it.","Ensure the React client submits via a `<form action={withState(action)}>` or `useActionState`, which always posts form data.","Guard `getActionState` behind a content-type check if the action handles multiple accept types."],"exampleFix":"// before\nexport const action = defineAction({ handler: async ({ request }) => {\n  const state = await getActionState({ request }); // throws for JSON\n}});\n// after\nexport const action = defineAction({\n  accept: 'form',\n  handler: async ({ request }) => {\n    const state = await getActionState({ request });\n  },\n});","handlingStrategy":"type-guard","validationCode":"function isFormRequest(request: Request): boolean {\n  const ct = request.headers.get('Content-Type') ?? '';\n  const base = ct.split(';')[0].toLowerCase();\n  return base === 'application/x-www-form-urlencoded' || base === 'multipart/form-data';\n}\nif (!isFormRequest(request)) { /* respond 415 or skip getActionState */ }","typeGuard":"function isFormContentType(request: Request): request is Request & { clone(): Promise<Request & { formData(): Promise<FormData> }> } {\n  const ct = (request.headers.get('Content-Type') ?? '').split(';')[0].toLowerCase();\n  return ct === 'application/x-www-form-urlencoded' || ct === 'multipart/form-data';\n}","tryCatchPattern":"try { await getActionState({ request }); } catch (e) { if (/must be called with a form request/) respondUnsupportedMedia(); else throw e; }","preventionTips":["Always declare `accept: 'form'` on actions that call `getActionState`.","Gate state retrieval on a content-type check before calling the API.","Keep a single code path (React form submission) for stateful actions."],"tags":["react","actions","forms","content-type"],"backgroundTag":null,"analyzedSha":"d081033d5fe8e8a68c4bbbad4af9d2deb9c74bca","analyzedAt":"2026-08-12T13:37:29.035Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}