thedotmack/claude-mem · error · Error

sync hub push: response is not JSON

Error message

sync hub push: response is not JSON

What it means

After a successful (2xx) push, pushOps parses the response body as JSON. If the body is not valid JSON — typically because an intermediary returned an HTML page or empty body with a 200 status — the client throws this error instead of proceeding to validate the acked array.

Solutions

  1. Verify hubUrl points at the actual sync hub endpoint, not a proxy or UI host.
  2. Check proxy/gateway config for pages that intercept 200 responses.
  3. Inspect raw response with curl to see what the hub actually returns.
  4. Redeploy or upgrade the hub so /v1/sync/ops returns a proper JSON PushResponse.

Example fix

// before
hubUrl: process.env.CLAUDE_MEM_CLOUD_SYNC_HUB_URL
// after
// ensure it is the API base, e.g.
hubUrl: 'https://sync.example.com' // serves JSON at /v1/sync/ops
Defensive patterns

Strategy: try-catch

Type guard

function isPushResponse(v: unknown): v is { acked: unknown[]; head_seq?: number } {
  return typeof v === 'object' && v !== null && Array.isArray((v as any).acked);
}

Try / catch

try { await client.pushOps(ops); } catch (e) { if (e.message === 'sync hub push: response is not JSON') { /* check hubUrl / proxy config */ } }

Prevention

When it happens

Trigger: Hub or a proxy/gateway returns 200 with a non-JSON body (HTML login page, empty body, CORS/error page) for POST /v1/sync/ops.

Common situations: Misconfigured reverse proxy intercepting requests; hub mis-deployed behind an auth wall; wrong hubUrl pointing at a non-sync server; network appliance rewriting responses.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/71340f66efa54e49. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sync/CloudSync.ts:1202

    // 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 d8bc9755e7)