{"record":{"id":"3a4587a3fe22da34","repo":"thedotmack/claude-mem","slug":"sync-hub-pull-res-status-body","errorCode":null,"errorMessage":"sync hub pull ${res.status}: ${body}","messagePattern":"sync hub pull (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/SyncClient.ts","lineNumber":550,"sourceCode":"            // Plain short request — never a held connection (directive #4).\n            signal: AbortSignal.timeout(Math.max(1, Math.min(this.requestTimeoutMs, remaining))),\n          }\n        );\n        // Kill-switch mode hint (plan Phase 5 task 2), read BEFORE the\n        // ok-check — the header rides error responses too. Asymmetric on\n        // purpose: header PRESENCE means poll regardless of status, but\n        // header ABSENCE only means \"cleared\" on an OK response. An error\n        // response without the header (a degraded auth upstream 503ing\n        // everything mid-incident — incidents correlate) is ambiguous and\n        // must not exit poll mode, or the client would resume socket\n        // churn for the whole outage.\n        const syncMode = res.headers.get('X-Sync-Mode');\n        if (syncMode !== null || res.ok) {\n          this.onSyncModeHint(syncMode);\n        }\n        if (!res.ok) {\n          const body = (await res.text().catch(() => '')).slice(0, 200);\n          throw new Error(`sync hub pull ${res.status}: ${body}`);\n        }\n        const page = await res.json() as ChangesPage | null;\n        if (!page || page.protocol_version !== 2 || !Array.isArray(page.ops)) {\n          throw new Error('sync hub pull: malformed /changes response');\n        }\n        const epoch = assertCanonicalDecimal(page.epoch);\n        assertCanonicalDecimal(page.head_seq);\n        if (typeof page.more !== 'boolean') throw new Error('sync hub pull: more must be boolean');\n        if (this.stopped) return;\n\n        const decodedOps = decodeChanges(page.ops);\n        const result = this.apply.applyOps(decodedOps, {\n          epoch,\n          requireContiguous: true,\n        });\n        pages++;\n\n        if (result.epochReset) {","sourceCodeStart":532,"sourceCodeEnd":568,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/services/sync/SyncClient.ts#L532-L568","documentation":"SyncClient's HTTP pull loop received a non-OK status from the sync hub's /changes endpoint; the error embeds the status code and the first 200 bytes of the body. The X-Sync-Mode header hint is processed before this throw, so poll-mode state is preserved across transient outages by design. This is the generic transport-level failure for hub pulls.","triggerScenarios":"GET /changes returning 401/403 (expired or revoked sync credentials), 503 (degraded auth upstream mid-incident), 429 (rate limited), 404 (wrong hub base URL), or any 5xx while the hub is restarting; corporate proxy or captive portal intercepting the request.","commonSituations":"Auth token expired overnight and every pull now 401s; hub deployed behind a load balancer that 503s during rollout; DNS pointing at the wrong environment; local firewall blocking the hub host.","solutions":["Check the embedded status: 401/403 means re-authenticate/refresh the sync credentials; 429 means back off (the client already retries with backoff).","Verify the hub base URL and network path (curl the /changes endpoint with the same credentials).","For 5xx, confirm hub health and retry once the incident clears — poll mode survives the outage automatically.","If a proxy intercepts responses, bypass it or add the hub host to NO_PROXY."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// Pre-flight the hub before entering the pull loop.\nasync function hubReachable(baseUrl: string, token: string): Promise<boolean> {\n  const res = await fetch(`${baseUrl}/changes?cursor=0&limit=1`, {\n    headers: { Authorization: `Bearer ${token}` },\n  });\n  return res.ok || res.status === 429; // 429 means alive but throttled\n}","typeGuard":null,"tryCatchPattern":"try {\n  await pullPage();\n} catch (e) {\n  if (e instanceof Error && /sync hub pull (401|403)/.test(e.message)) {\n    await refreshCredentials(); // hard stop is wrong for auth: fix creds, then retry\n  } else if (e instanceof Error && e.message.startsWith('sync hub pull')) {\n    await sleep(backoffMs()); // 5xx/429: exponential backoff, poll mode survives\n  } else throw e;\n}","preventionTips":["Keep sync credentials auto-refreshed before they expire instead of relying on the 401 path.","Give the hub a dedicated health check so outages are visible before pulls fail.","Classify statuses: only 401/403 need credential action; everything else is retryable."],"tags":["sync","http","network","authentication","retry"],"backgroundTag":"http-error-response","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","contentChangedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}