{"record":{"id":"eb5ef0b205ea5b4c","repo":"coleam00/Archon","slug":"request-body-is-not-valid-json-send-reason","errorCode":null,"errorMessage":"Request body is not valid JSON — send {\"reason\": \"...\"} or no body","messagePattern":"Request body is not valid JSON — send (.+?) or no body","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"packages/server/src/routes/api.ts","lineNumber":3986,"sourceCode":"      const rejectBlocker = pausedGateBlocker(\n        run,\n        'Reject the child run instead, or abandon this run to discard the whole tree.',\n        false\n      );\n      if (rejectBlocker) {\n        return apiError(c, 400, rejectBlocker);\n      }\n      // Mirror of the approve route's malformed-body guard: a swallowed parse\n      // failure would silently drop the reviewer's reason.\n      const rawBody = await c.req.text();\n      let body: { reason?: string } = {};\n      if (rawBody.trim().length > 0) {\n        try {\n          body = JSON.parse(rawBody) as { reason?: string };\n        } catch (parseError) {\n          getLog().warn({ err: parseError, runId }, 'api.reject_body_parse_failed');\n          return apiError(\n            c,\n            400,\n            'Request body is not valid JSON — send {\"reason\": \"...\"} or no body'\n          );\n        }\n      }\n      const reason = body.reason ?? 'Rejected';\n      // Shared gate logic (events, telemetry, staging/cancel decision). When an\n      // on_reject rework is staged the run stays 'paused' with\n      // metadata.approval.resolved = 'rejected' (#2075).\n      const result = await rejectWorkflow(runId, reason);\n\n      if (result.cancelled) {\n        return c.json({\n          success: true,\n          message: result.maxAttemptsReached\n            ? `Workflow rejected and cancelled (max attempts reached): ${run.workflow_name}`\n            : `Workflow rejected: ${run.workflow_name}`,\n        });","sourceCodeStart":3968,"sourceCodeEnd":4004,"githubUrl":"https://github.com/coleam00/Archon/blob/0773b9745896ef0612e709c80845a0f7db315b19/packages/server/src/routes/api.ts#L3968-L4004","documentation":"The server's POST reject endpoint (packages/server/src/routes/api.ts:3986) accepts an optional JSON body of shape {reason: string}. A non-empty body that fails JSON.parse is rejected with 400 and the message points the caller at the expected shape; it does not treat raw text as the reason.","triggerScenarios":"POSTing to the run-reject route with a non-empty raw body that is not valid JSON (bare text reason, trailing commas, unescaped quotes, HTML body). Empty body is allowed and defaults reason to 'Rejected'.","commonSituations":"curl -d 'timing issue' without JSON formatting; webhook/automation tools sending form-encoded bodies; quotes inside the reason string breaking the JSON envelope; copy-pasted JSON with smart quotes.","solutions":["Send valid JSON: {\"reason\": \"...\"} with Content-Type: application/json","Omit the body — the endpoint supplies the default reason 'Rejected'","Check the server log keyed 'api.reject_body_parse_failed' for the exact parse error","Validate the payload locally with JSON.parse before sending"],"exampleFix":"// before\ncurl -X POST $URL/runs/$RUN/reject -d 'timing issue'\n// after\ncurl -X POST $URL/runs/$RUN/reject -H 'Content-Type: application/json' -d '{\"reason\":\"timing issue\"}'","handlingStrategy":"validation","validationCode":"function buildRejectBody(reason) {\n  if (reason === undefined) return null; // server defaults to 'Rejected'\n  const json = JSON.stringify({ reason });\n  JSON.parse(json);\n  return json;\n}","typeGuard":"function isRejectBody(b: unknown): b is { reason?: string } {\n  return typeof b === 'object' && b !== null &&\n    (!('reason' in b) || typeof (b as any).reason === 'string');\n}","tryCatchPattern":"try {\n  const res = await fetch(rejectUrl, { method:'POST', headers:{'Content-Type':'application/json'}, body: bodyJson ?? undefined });\n  if (!res.ok) throw new Error(`reject failed: ${res.status}`);\n} catch (e) { /* surface to operator; do not silently retry */ }","preventionTips":["Serialize with JSON.stringify so quotes/newlines in the reason are escaped","Never pass raw prose as a request body","Omit the body to use the default 'Rejected' reason","Check for smart quotes when copying JSON from docs"],"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"}