thedotmack/claude-mem · error

sync hub status: response is not JSON

Error message

sync hub status: response is not JSON

What it means

Thrown by probeHubStatus when response.ok but response.json() threw — the hub returned 200 with a body that is not valid JSON (or the stream errored). This is a protocol/contract violation: a 200 on /v1/sync/status must be a JSON object.

Source

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

          'X-User-Id': this.userId,
          'X-Device-Id': this.deviceId,
          ...(this.deviceName ? { 'X-Device-Name': this.deviceName } : {}),
        },
        signal: AbortSignal.timeout(this.requestTimeoutMs),
      });
      checkedAt = Date.now();
      const syncMode = response.headers.get('X-Sync-Mode');
      if (syncMode !== null || response.ok) this.emitSyncMode(syncMode);
      if (!response.ok) {
        const body = (await response.text().catch(() => '')).slice(0, 200);
        throw new Error(`sync hub status ${response.status}: ${body}`);
      }

      let parsed: unknown;
      try {
        parsed = await response.json();
      } catch {
        throw new Error('sync hub status: response is not JSON');
      }
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new Error('sync hub status: response must be an object');
      }
      const record = parsed as Record<string, unknown>;
      if (record.protocol_version !== 2) {
        throw new Error('sync hub status: response requires protocol_version 2');
      }
      if (
        typeof record.epoch !== 'string'
        || typeof record.head_seq !== 'string'
        || typeof record.projected_seq !== 'string'
      ) {
        throw new Error('sync hub status: response requires decimal-string epoch/head_seq/projected_seq');
      }
      const epoch = assertCanonicalDecimal(record.epoch, { positive: true });
      const headSeq = assertCanonicalDecimal(record.head_seq);
      const projectedSeq = assertCanonicalDecimal(record.projected_seq);

View on GitHub (pinned to d768ba3643)

Solutions

  1. Capture the raw response body and Content-Type header to see what the hub actually returned.
  2. Verify hubUrl points at the real sync hub and not a generic web server / captive portal.
  3. Check for an intercepting proxy/gateway returning HTML; bypass it or fix its routing.
  4. If the hub itself is serving non-JSON on 200, that is a hub bug — report it to the hub operator.
  5. Ensure no middleware rewrites 5xx into 200-with-HTML.

Example fix

// before: hubUrl hits a generic web server
hubUrl = 'https://example.com';
// after: hubUrl hits the actual sync hub API base
hubUrl = 'https://sync.example.com'; // serves /v1/sync/status as JSON
Defensive patterns

Strategy: validation

Type guard

function isHubStatusNotJson(e: unknown): boolean {
  return e instanceof Error && /sync hub status: response is not JSON/i.test(e.message);
}

Try / catch

try { await cloudSync.status(); }
catch (e) { if (isHubStatusNotJson(e)) { markHubMisconfigured(e.message); return; } throw e; }

Prevention

When it happens

Trigger: The hub returned 200 but the body was HTML (error page from a misconfigured proxy/CDN), empty, truncated, or text; a transparent proxy returned a captive-portal page; the response Content-Type was not application/json and the body was not parseable.

Common situations: A reverse proxy/CDN intercepting the request and returning an HTML error page with status 200; hub misconfigured to serve a status page instead of JSON; network middlebox truncating the body; pointing hubUrl at a non-hub host.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/70acff8c9a3019aa. Report an issue: GitHub.