{"record":{"id":"89f1c1915a9daddf","repo":"thedotmack/claude-mem","slug":"pull-res-status-await-res-text-catch-slice-0-200","errorCode":null,"errorMessage":"pull ${res.status}: ${(await res.text().catch(() => \"\")).slice(0, 200)}","messagePattern":"pull (.+?): (.+?)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workers/sync-hub/canary/canary.ts","lineNumber":261,"sourceCode":"\t\t\tacked: Array<{ operation_sha256: string; seq: string }>;\n\t\t\thead_seq: string;\n\t\t};\n\t\tconst ack = parsed.acked.find((a) => a.operation_sha256 === operationSha256);\n\t\tif (!ack) throw new Error(\"push response did not ack the canary op\");\n\t\treturn { seq: ack.seq, headSeq: parsed.head_seq, opSha: operationSha256 };\n\t}\n\n\tprivate async pull(\n\t\tdeviceId: string,\n\t\tsince: string,\n\t): Promise<{ ops: Array<{ seq: string; operation_sha256: string }>; headSeq: string }> {\n\t\tconst res = await fetch(`${this.args.hub}/v1/sync/changes?since=${since}&limit=500`, {\n\t\t\theaders: this.headers(deviceId),\n\t\t\tsignal: AbortSignal.timeout(this.args.timeoutMs),\n\t\t});\n\t\tthis.noteMode(res);\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(`pull ${res.status}: ${(await res.text().catch(() => \"\")).slice(0, 200)}`);\n\t\t}\n\t\tconst parsed = (await res.json()) as {\n\t\t\tops: Array<{ seq: string; operation_sha256: string }>;\n\t\t\thead_seq: string;\n\t\t};\n\t\treturn { ops: parsed.ops, headSeq: parsed.head_seq };\n\t}\n\n\t/** Start both cursors at the current head — only NEW ops matter. */\n\tasync init(): Promise<void> {\n\t\tconst res = await fetch(`${this.args.hub}/v1/sync/status`, {\n\t\t\theaders: this.headers(DEVICE_A),\n\t\t\tsignal: AbortSignal.timeout(this.args.timeoutMs),\n\t\t});\n\t\tthis.noteMode(res);\n\t\tif (!res.ok) {\n\t\t\tthrow new Error(`status ${res.status}: ${(await res.text().catch(() => \"\")).slice(0, 200)}`);\n\t\t}","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/canary/canary.ts#L243-L279","documentation":"Thrown by the canary's `pull` method when the `/v1/sync/changes` endpoint returns a non-2xx status. The message embeds the HTTP status and the first 200 bytes of the response body to expose the server's error detail. It signals the incremental change fetch failed and the canary's replication check cannot proceed for this cycle.","triggerScenarios":"Any `fetch(`${hub}/v1/sync/changes?since=...&limit=500`)` response with `res.ok === false`: 401/403 for a bad or rotated device token, 400 for a malformed `since` cursor, 404 after a hub epoch reset wiped the cursor, 429 rate limiting, or 5xx from an unhealthy hub worker.","commonSituations":"Device credentials expired or were rotated in the hub config; the `since` cursor references a seq trimmed by the server (epoch bump / compaction); Cloudflare Worker upstream timeouts; or the canary is pointed at the wrong hub URL via args.","solutions":["Read the status and body in the message: 401/403 → fix device credentials; 400 → reset the `since` cursor; 5xx → check hub worker logs.","On 404 or cursor-related errors, re-run `init` to fetch a fresh `head_seq` and reset both device cursors.","Retry transient statuses (429, 500, 502, 503) with exponential backoff before failing the canary run.","Verify `this.args.hub` points at the correct deployed hub and that `this.headers(deviceId)` carries a valid token."],"exampleFix":"// before\nif (!res.ok) {\n  throw new Error(`pull ${res.status}: ${(await res.text().catch(() => \"\")).slice(0, 200)}`);\n}\n\n// after\nif (res.status === 429 || res.status >= 500) {\n  await sleep(backoffMs);\n  return this.pull(deviceId, since); // bounded retry\n}\nif (!res.ok) {\n  throw new Error(`pull ${res.status}: ${(await res.text().catch(() => \"\")).slice(0, 200)}`);\n}","handlingStrategy":"retry","validationCode":"// before pulling, verify reachability and cursor sanity\nif (!hubUrl.startsWith('https://')) throw new Error('invalid hub url');\nif (!Number.isFinite(Number(since)) && since !== '0') console.warn('suspicious since cursor: ' + since);\nconst status = await fetch(`${hubUrl}/v1/sync/status`, { headers });\nif (!status.ok) throw new Error(`hub unhealthy pre-pull: ${status.status}`);","typeGuard":"function isPullSuccess(res: Response): boolean {\n  return res.ok && res.headers.get('content-type')?.includes('application/json') === true;\n}","tryCatchPattern":"try {\n  const { ops, headSeq } = await canary.pull(deviceId, since);\n} catch (err) {\n  if (err instanceof Error && /^pull \\d{3}:/.test(err.message)) {\n    const status = Number(err.message.slice(5, 8));\n    if (status === 429 || status >= 500) await retryWithBackoff();\n    else if (status === 404 || status === 400) await resetCursorViaInit();\n    else throw err;\n  } else throw err;\n}","preventionTips":["Re-init cursors whenever the hub epoch changes or a 404/400 cursor error appears.","Rotate device tokens in sync with hub secret updates.","Cap `limit` and page frequency to stay under hub rate limits.","Monitor hub worker 5xx rates so pull failures are caught upstream first."],"tags":["http","network","sync","error-response"],"backgroundTag":"http-error-response","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}