thedotmack/claude-mem · error · Error

sync hub status: response is not JSON

Error message

sync hub status: response is not JSON

What it means

probeHubStatus expects the hub status endpoint to return a JSON body. If response.json() throws — e.g. an HTML error page, empty body, or non-JSON content — it throws 'sync hub status: response is not JSON'. This guards the protocol client against talking to a non-conforming endpoint.

Solutions

  1. Verify the hub status URL points to the JSON status API endpoint, not a UI or proxy page.
  2. Check for redirects/auth walls: log response.url and status before parsing.
  3. Ensure the hub serves Content-Type application/json and a valid JSON body.
  4. Check network middleboxes (corporate proxies, VPNs) that may inject HTML error pages.
  5. If an HTML page is returned, surface the first bytes in logs to identify what actually answered.

Example fix

// before: blind probe
const res = await fetch(hubUrl + '/status');
// after: verify endpoint returns JSON before probing
const res = await fetch(hubUrl + '/status', { redirect: 'follow' });
if ((res.headers.get('content-type') || '').includes('application/json')) {
  const status = await cloudSync.statusWithHubProbe();
} else {
  logger.warn('hub status endpoint did not return JSON', { url: res.url });
}
Defensive patterns

Strategy: validation

Validate before calling

const res = await fetch(hubUrl + '/status');
const ct = res.headers.get('content-type') || '';
if (!ct.includes('application/json')) {
  throw new Error(`hub status returned non-JSON (${ct}): ${(await res.text()).slice(0, 100)}`);
}

Try / catch

try {
  const status = await cloudSync.statusWithHubProbe();
} catch (e) {
  if (e.message === 'sync hub status: response is not JSON') {
    logger.error('hub status endpoint not JSON — check URL/redirects/proxies', { url: hubUrl });
    fallBackToLocalStatus();
  } else throw e;
}

Prevention

When it happens

Trigger: Hub returns text/html (proxy error page, login redirect), an empty body, or wrong Content-Type so response.json() fails to parse.

Common situations: Wrong URL pointing at a web UI instead of the API, auth middleware redirecting to a login page, proxy/captive-portal injecting HTML, hub returning a 200 with empty body.

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/70acff8c9a3019aa. Report an issue: GitHub.

Appendix: source

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

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