{"record":{"id":"71340f66efa54e49","repo":"thedotmack/claude-mem","slug":"sync-hub-push-response-is-not-json","errorCode":null,"errorMessage":"sync hub push: response is not JSON","messagePattern":"sync hub push: response is not JSON","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":975,"sourceCode":"    // paths would keep hammering the socket through an incident.\n    // Asymmetric on purpose (SyncClient.onSyncModeHint contract): header\n    // PRESENCE is emitted regardless of status; header ABSENCE is only\n    // emitted (as null = \"cleared\") from an OK response — absence on an\n    // error response is ambiguous (a degraded auth upstream 503s without\n    // the funnel) and must not read as \"switch cleared\".\n    const syncMode = res.headers.get('X-Sync-Mode');\n    if (syncMode !== null || res.ok) {\n      this.emitSyncMode(syncMode);\n    }\n    if (!res.ok) {\n      const body = (await res.text().catch(() => '')).slice(0, 200);\n      throw new Error(`sync hub push ${res.status}: ${body}`);\n    }\n    let parsed: unknown;\n    try {\n      parsed = await res.json();\n    } catch {\n      throw new Error('sync hub push: response is not JSON');\n    }\n    const acked = (parsed as { acked?: unknown } | null)?.acked;\n    if (!Array.isArray(acked)) {\n      throw new Error('sync hub push: response missing acked array');\n    }\n    const headSeq = (parsed as { head_seq?: unknown }).head_seq;\n    const projectedSeq = (parsed as { projected_seq?: unknown }).projected_seq;\n    if (typeof headSeq !== 'string' || typeof projectedSeq !== 'string') {\n      throw new Error('sync hub push: response requires decimal-string head_seq/projected_seq');\n    }\n    assertCanonicalDecimal(headSeq);\n    assertCanonicalDecimal(projectedSeq);\n    const validatedAcked = acked.map((value, index): AckedOp => {\n      if (!value || typeof value !== 'object' || Array.isArray(value)) {\n        throw new Error(`sync hub push: acked[${index}] must be an object`);\n      }\n      const item = value as Record<string, unknown>;\n      if (","sourceCodeStart":957,"sourceCodeEnd":993,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/sync/CloudSync.ts#L957-L993","documentation":"Thrown when the push POST returned OK but response.json() threw — a 200 with a non-JSON body. The push contract requires the hub to return a JSON object ({ acked, head_seq, projected_seq }) on success, so a non-JSON 200 is a protocol violation.","triggerScenarios":"Hub returned 200 with HTML/empty/truncated body; a proxy rewrote the response; the endpoint routed to a static server; the connection dropped mid-body so json() failed.","commonSituations":"Reverse proxy/CDN returning an HTML page with 200; hub misconfigured to acknowledge pushes without a JSON body; middlebox truncation; pointing hubUrl at the wrong host.","solutions":["Capture the raw response body and Content-Type for the push to identify what was returned.","Verify hubUrl/routes the POST to the real sync hub push endpoint.","Bypass or fix any proxy returning HTML on a 200.","If the hub itself is non-JSON on 200, report it — the push contract requires JSON.","Retry once in case of a transient mid-body truncation."],"exampleFix":"// before: hubUrl points at a server that acks pushes with an empty 200\nhubUrl = 'https://example.com';\n// after: hubUrl points at the sync hub that returns JSON acks\nhubUrl = 'https://sync.example.com'; // POST /v1/sync/ops -> {\"acked\":[...],\"head_seq\":\"...\",...}","handlingStrategy":"validation","validationCode":null,"typeGuard":"function isPushNotJson(e: unknown): boolean {\n  return e instanceof Error && /sync hub push: response is not JSON/i.test(e.message);\n}","tryCatchPattern":"try { await cloudSync.pushOps(ops); }\ncatch (e) { if (isPushNotJson(e)) { markHubMisconfigured(e.message); return; } throw e; }","preventionTips":["Ensure the POST reaches the real sync hub push endpoint that returns JSON acks.","Fix proxies/CDNs returning HTML bodies on 200 responses.","Contract-test the push response shape against the hub.","Capture the raw body once to identify the middlebox responsible."],"tags":["cloud-sync","push","json","hub","protocol"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}