coleam00/Archon · error

Request body is not valid JSON — send {"reason": "..."} or n

Error message

Request body is not valid JSON — send {"reason": "..."} or no body

What it means

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.

Source

Thrown at packages/server/src/routes/api.ts:3986

      const rejectBlocker = pausedGateBlocker(
        run,
        'Reject the child run instead, or abandon this run to discard the whole tree.',
        false
      );
      if (rejectBlocker) {
        return apiError(c, 400, rejectBlocker);
      }
      // Mirror of the approve route's malformed-body guard: a swallowed parse
      // failure would silently drop the reviewer's reason.
      const rawBody = await c.req.text();
      let body: { reason?: string } = {};
      if (rawBody.trim().length > 0) {
        try {
          body = JSON.parse(rawBody) as { reason?: string };
        } catch (parseError) {
          getLog().warn({ err: parseError, runId }, 'api.reject_body_parse_failed');
          return apiError(
            c,
            400,
            'Request body is not valid JSON — send {"reason": "..."} or no body'
          );
        }
      }
      const reason = body.reason ?? 'Rejected';
      // Shared gate logic (events, telemetry, staging/cancel decision). When an
      // on_reject rework is staged the run stays 'paused' with
      // metadata.approval.resolved = 'rejected' (#2075).
      const result = await rejectWorkflow(runId, reason);

      if (result.cancelled) {
        return c.json({
          success: true,
          message: result.maxAttemptsReached
            ? `Workflow rejected and cancelled (max attempts reached): ${run.workflow_name}`
            : `Workflow rejected: ${run.workflow_name}`,
        });

View on GitHub (pinned to 0773b97458)

Solutions

  1. Send valid JSON: {"reason": "..."} with Content-Type: application/json
  2. Omit the body — the endpoint supplies the default reason 'Rejected'
  3. Check the server log keyed 'api.reject_body_parse_failed' for the exact parse error
  4. Validate the payload locally with JSON.parse before sending

Example fix

// before
curl -X POST $URL/runs/$RUN/reject -d 'timing issue'
// after
curl -X POST $URL/runs/$RUN/reject -H 'Content-Type: application/json' -d '{"reason":"timing issue"}'
Defensive patterns

Strategy: validation

Validate before calling

function buildRejectBody(reason) {
  if (reason === undefined) return null; // server defaults to 'Rejected'
  const json = JSON.stringify({ reason });
  JSON.parse(json);
  return json;
}

Type guard

function isRejectBody(b: unknown): b is { reason?: string } {
  return typeof b === 'object' && b !== null &&
    (!('reason' in b) || typeof (b as any).reason === 'string');
}

Try / catch

try {
  const res = await fetch(rejectUrl, { method:'POST', headers:{'Content-Type':'application/json'}, body: bodyJson ?? undefined });
  if (!res.ok) throw new Error(`reject failed: ${res.status}`);
} catch (e) { /* surface to operator; do not silently retry */ }

Prevention

When it happens

Trigger: 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'.

Common situations: 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.

Related errors


AI-assisted analysis of coleam00/Archon@0773b97458 (2026-09-01). Data as JSON: /api/errors/eb5ef0b205ea5b4c. Report an issue: GitHub.