{"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/52e6c34790cc8ac4e69e6135ace06049867e5c4a/packages/integrations/react/src/actions.ts#L32-L68","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst action = defineAction({ handler: async ({ request }) => { const s = await getActionState({ request }); return s; } });\n// after\nconst action = defineAction({ accept: 'form', handler: async ({ request }) => { const s = await getActionState({ request }); return s; } });","handlingStrategy":"validation","validationCode":"const ct = request.headers.get('Content-Type') ?? '';\nconst isForm = ct.startsWith('application/x-www-form-urlencoded') || ct.startsWith('multipart/form-data');\nif (!isForm) throw new Response('Expected a form submission', { status: 415 });\nconst state = await getActionState({ request });","typeGuard":"function isFormRequest(contentType: string): boolean {\n  const base = contentType.split(';')[0].trim();\n  return base === 'application/x-www-form-urlencoded' || base === 'multipart/form-data';\n}","tryCatchPattern":"try {\n  state = await getActionState({ request });\n} catch (err) {\n  if (String(err?.message).includes('form request')) {\n    state = null; // JSON submission path; no React state expected\n  } else throw err;\n}","preventionTips":["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"],"tags":["react","actions","forms","content-type"],"backgroundTag":"wrong-content-type","analyzedSha":"52e6c34790cc8ac4e69e6135ace06049867e5c4a","analyzedAt":"2026-08-18T18:48:03.901Z","contentChangedAt":"2026-08-18T18:48:03.901Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}