thedotmack/claude-mem · error
sync hub push: response is not JSON
Error message
sync hub push: response is not JSON
What it means
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.
Source
Thrown at src/services/sync/CloudSync.ts:975
// paths would keep hammering the socket through an incident.
// Asymmetric on purpose (SyncClient.onSyncModeHint contract): header
// PRESENCE is emitted regardless of status; header ABSENCE is only
// emitted (as null = "cleared") from an OK response — absence on an
// error response is ambiguous (a degraded auth upstream 503s without
// the funnel) and must not read as "switch cleared".
const syncMode = res.headers.get('X-Sync-Mode');
if (syncMode !== null || res.ok) {
this.emitSyncMode(syncMode);
}
if (!res.ok) {
const body = (await res.text().catch(() => '')).slice(0, 200);
throw new Error(`sync hub push ${res.status}: ${body}`);
}
let parsed: unknown;
try {
parsed = await res.json();
} catch {
throw new Error('sync hub push: response is not JSON');
}
const acked = (parsed as { acked?: unknown } | null)?.acked;
if (!Array.isArray(acked)) {
throw new Error('sync hub push: response missing acked array');
}
const headSeq = (parsed as { head_seq?: unknown }).head_seq;
const projectedSeq = (parsed as { projected_seq?: unknown }).projected_seq;
if (typeof headSeq !== 'string' || typeof projectedSeq !== 'string') {
throw new Error('sync hub push: response requires decimal-string head_seq/projected_seq');
}
assertCanonicalDecimal(headSeq);
assertCanonicalDecimal(projectedSeq);
const validatedAcked = acked.map((value, index): AckedOp => {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`sync hub push: acked[${index}] must be an object`);
}
const item = value as Record<string, unknown>;
if (View on GitHub (pinned to d768ba3643)
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.
Example fix
// before: hubUrl points at a server that acks pushes with an empty 200
hubUrl = 'https://example.com';
// after: hubUrl points at the sync hub that returns JSON acks
hubUrl = 'https://sync.example.com'; // POST /v1/sync/ops -> {"acked":[...],"head_seq":"...",...} Defensive patterns
Strategy: validation
Type guard
function isPushNotJson(e: unknown): boolean {
return e instanceof Error && /sync hub push: response is not JSON/i.test(e.message);
} Try / catch
try { await cloudSync.pushOps(ops); }
catch (e) { if (isPushNotJson(e)) { markHubMisconfigured(e.message); return; } throw e; } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- sync hub status: response is not JSON
- sync hub status: response must be an object
- sync hub push: response missing acked array
- sync hub status: response requires decimal-string epoch/head
- sync hub push ${res.status}: ${body}
AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12).
Data as JSON: /api/errors/71340f66efa54e49.
Report an issue: GitHub.