{"record":{"id":"b3cfa253d85014b5","repo":"remix-run/react-router","slug":"cannot-handle-form-actions-without-a-decodeaction","errorCode":null,"errorMessage":"Cannot handle form actions without a decodeAction function","messagePattern":"Cannot handle form actions without a decodeAction function","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/react-router/lib/rsc/server.rsc.ts","lineNumber":730,"sourceCode":"    let formData =\n      maybeFormData &&\n      typeof maybeFormData === \"object\" &&\n      maybeFormData instanceof FormData\n        ? maybeFormData\n        : null;\n\n    let skipRevalidation = formData?.has(\"$SKIP_REVALIDATION\") ?? false;\n\n    return {\n      actionResult,\n      revalidationRequest: getRevalidationRequest(),\n      skipRevalidation,\n    };\n  } else if (isFormRequest) {\n    const formData = await request.clone().formData();\n    if (Array.from(formData.keys()).some((k) => k.startsWith(\"$ACTION_\"))) {\n      if (!decodeAction) {\n        throw new Error(\n          \"Cannot handle form actions without a decodeAction function\",\n        );\n      }\n      const action = await decodeAction(formData);\n      let formState = undefined;\n      try {\n        let result = await action();\n        if (isRedirectResponse(result)) {\n          result = prependBasenameToRedirectResponse(result, basename);\n        }\n        formState = await decodeFormState?.(result, formData);\n      } catch (error) {\n        if (isRedirectResponse(error)) {\n          return prependBasenameToRedirectResponse(error, basename);\n        }\n        if (isResponse(error)) {\n          return error;\n        }","sourceCodeStart":712,"sourceCodeEnd":748,"githubUrl":"https://github.com/remix-run/react-router/blob/7aea711dd1ae2bc5a076d13ff17291829690fa74/packages/react-router/lib/rsc/server.rsc.ts#L712-L748","documentation":"Progressive-enhancement form posts (no JS) carry $ACTION_* keys in the FormData. Handling them requires React's decodeAction to rebuild the action from those fields; without it, processServerAction throws (lib/rsc/server.rsc.ts:723). decodeFormState is optional, decodeAction is not.","triggerScenarios":"A form POST with $ACTION_* fields (JS disabled, curl test, bot, or a plain HTML POST) hitting an RSC handler created without decodeAction; smoke-testing forms via curl against a partially wired custom RSC server.","commonSituations":"Custom RSC server setups that wired only the fetch-action path (decodeReply) but not the form fallback; CI/curl tests that POST form encodings; crawlers replaying form submissions.","solutions":["Pass decodeAction (and decodeFormState for useFormState support) from the RSC runtime into the server handler options","Verify with a curl POST containing a $ACTION_ key that the server no longer throws","If progressive enhancement is out of scope, ship forms that only submit via JS and document the limitation"],"exampleFix":"// before\nconst handler = createRequestHandler({ build, decodeReply, loadServerAction })\n// after\nconst handler = createRequestHandler({\n  build,\n  decodeReply,\n  loadServerAction,\n  decodeAction,\n  decodeFormState,\n})","handlingStrategy":"validation","validationCode":"// Startup check for progressive enhancement support\nif (typeof decodeAction !== 'function') {\n  console.warn('No-JS form submissions ($ACTION_*) will throw; pass decodeAction to the RSC handler');\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pass decodeAction (and decodeFormState) alongside decodeReply when wiring custom RSC servers","Include a curl form-POST smoke test with a $ACTION_ key in CI","Keep handler wiring in one place so option sets stay in sync across upgrades"],"tags":["rsc","server-actions","forms","progressive-enhancement"],"backgroundTag":"missing-rsc-handler-config","analyzedSha":"7aea711dd1ae2bc5a076d13ff17291829690fa74","analyzedAt":"2026-08-18T18:04:14.938Z","contentChangedAt":"2026-08-18T18:04:14.938Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}