{"record":{"id":"7ece07eb6f90649d","repo":"coleam00/Archon","slug":"request-body-is-not-valid-json-send-decision","errorCode":null,"errorMessage":"Request body is not valid JSON — send {\"decision\": \"...\", \"text\": \"...\"}","messagePattern":"Request body is not valid JSON — send (.+?)","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/server/src/routes/api.ts","lineNumber":4061,"sourceCode":"        return apiError(c, 400, `Cannot respond to workflow in '${run.status}' status`);\n      }\n      const respondBlocker = pausedGateBlocker(\n        run,\n        'Respond on the child run instead, or abandon this run to discard the whole tree.',\n        false\n      );\n      if (respondBlocker) {\n        return apiError(c, 400, respondBlocker);\n      }\n      const rawBody = await c.req.text();\n      let body: { decision?: string; text?: string } = {};\n      if (rawBody.trim().length > 0) {\n        try {\n          body = JSON.parse(rawBody) as { decision?: string; text?: string };\n        } catch (parseError) {\n          getLog().warn({ err: parseError, runId }, 'api.respond_body_parse_failed');\n          return apiError(\n            c,\n            400,\n            'Request body is not valid JSON — send {\"decision\": \"...\", \"text\": \"...\"}'\n          );\n        }\n      }\n      if (!body.decision) {\n        return apiError(c, 400, 'Request body must include a non-empty \"decision\"');\n      }\n      const decision = body.decision;\n\n      // Pre-validate a non-default decision so an undeclared id is a 400 naming the\n      // gate's actual options, not an opaque 500 — mirrors the approve/reject routes'\n      // pre-checks above. 'approve'/'reject' skip this: assertRespondable enforces\n      // decisionsAuthored, which legacy gates (the ones those two ids also serve)\n      // never set — see assertRespondable's doc comment for why it is not consulted\n      // for those two ids.\n      if (decision !== 'approve' && decision !== 'reject') {\n        try {","sourceCodeStart":4043,"sourceCodeEnd":4079,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/server/src/routes/api.ts#L4043-L4079","documentation":"The server's POST respond endpoint (packages/server/src/routes/api.ts:4061), used to answer a durable wait or interactive node, expects an optional JSON body of shape {decision: string, text: string}. A non-empty body that fails JSON.parse yields this 400; after parsing, a missing decision is validated separately.","triggerScenarios":"POSTing a response to a run's wait/gate with a non-empty raw body that is not valid JSON (raw decision word, unescaped quotes in the text, form-encoded or plain-text body).","commonSituations":"curl -d 'approve' instead of JSON; UI clients stringifying the payload twice or not at all; text containing newlines/quotes pasted into hand-built JSON; proxies rewriting the body.","solutions":["Send valid JSON: {\"decision\": \"approve\", \"text\": \"...\"} with Content-Type: application/json","Build the payload with JSON.stringify({decision, text}) instead of string concatenation","Omit the body only if the endpoint's no-body path is intended","Check the server log keyed 'api.respond_body_parse_failed' for the exact parse error"],"exampleFix":"// before\nfetch(url, {method:'POST', body:`approve ${notes}`})\n// after\nfetch(url, {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({decision:'approve', text: notes})})","handlingStrategy":"validation","validationCode":"function buildRespondBody(decision, text) {\n  if (!decision) throw new TypeError('decision is required');\n  const json = JSON.stringify({ decision, text });\n  JSON.parse(json);\n  return json;\n}","typeGuard":"function isRespondBody(b: unknown): b is { decision?: string; text?: string } {\n  return typeof b === 'object' && b !== null &&\n    (!('decision' in b) || typeof (b as any).decision === 'string') &&\n    (!('text' in b) || typeof (b as any).text === 'string');\n}","tryCatchPattern":"try {\n  const res = await fetch(respondUrl, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({decision, text}) });\n  if (res.status === 400) throw new Error('respond body rejected; check decision/text shape');\n} catch (e) { /* log; re-prompt user for the decision */ }","preventionTips":["Always build the body with JSON.stringify, not template strings","Validate decision is present before sending (server checks it after parsing)","Set Content-Type: application/json","Test payloads containing newlines/quotes locally with JSON.parse"],"tags":["http","json","api","client-error"],"backgroundTag":"invalid-json-request-body","analyzedSha":"0773b9745896ef0612e709c80845a0f7db315b19","analyzedAt":"2026-09-01T02:28:07.064Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T05:18:18.240Z"}