{"record":{"id":"9818d4e5743804e1","repo":"thedotmack/claude-mem","slug":"sync-hub-push-invariant-request-exceeds-max-bod","errorCode":null,"errorMessage":"sync hub push invariant: request exceeds ${MAX_BODY_BYTES} encoded bytes","messagePattern":"sync hub push invariant: request exceeds (.+?) encoded bytes","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":1168,"sourceCode":"        this.db.prepare(`\n          UPDATE ${table} SET synced_at = NULL\n          WHERE id = ? AND origin_device_id IS NULL AND (synced_at IS NULL OR synced_at < 0)\n        `).run(row.origin_local_id);\n      }\n    });\n    tx();\n    logger.error('CLOUD_SYNC', 'Dropped stale content outbox snapshot; local row left unsynced for resnapshot', {\n      kind: row.kind,\n      originLocalId: row.origin_local_id,\n      entityRev: row.entity_rev,\n      reason,\n    });\n  }\n\n  private async pushOps(ops: WireOp[]): Promise<PushResponse> {\n    const requestBody = JSON.stringify({ protocol_version: 2, ops });\n    if (Buffer.byteLength(requestBody, 'utf8') > MAX_BODY_BYTES) {\n      throw new Error(`sync hub push invariant: request exceeds ${MAX_BODY_BYTES} encoded bytes`);\n    }\n    const res = await this.fetchImpl(`${this.hubUrl}/v1/sync/ops`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': `Bearer ${this.token}`,\n        'X-User-Id': this.userId,\n        'X-Device-Id': this.deviceId,\n        ...(this.deviceName ? { 'X-Device-Name': this.deviceName } : {}),\n      },\n      body: requestBody,\n      signal: AbortSignal.timeout(this.requestTimeoutMs),\n    });\n    // Mode hint BEFORE the ok-check: the kill-switch header rides error\n    // responses too, and a client that only learned the mode from happy\n    // paths would keep hammering the socket through an incident.\n    // Asymmetric on purpose (SyncClient.onSyncModeHint contract): header\n    // PRESENCE is emitted regardless of status; header ABSENCE is only","sourceCodeStart":1150,"sourceCodeEnd":1186,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/src/services/sync/CloudSync.ts#L1150-L1186","documentation":"pushOps enforces a hard limit on the encoded push request body (MAX_BODY_BYTES) before POSTing ops to the hub. If JSON.stringify of the batch exceeds the limit, the client refuses to send it, since the hub would reject oversized bodies. This is a client-side guard of an internal invariant about batch size.","triggerScenarios":"Calling the push path with a WireOp[] batch whose JSON body exceeds MAX_BODY_BYTES — e.g. many queued ops accumulated in sync_content_outbox flushed in one request.","commonSituations":"Long offline periods let the outbox grow; a large batch is drained in one push; deletes or large content updates inflate op payloads; MAX_BODY_BYTES was lowered on a hub/proxy upgrade while the client still batches aggressively.","solutions":["Reduce the ops batch size — chunk the WireOp[] into batches under MAX_BODY_BYTES and push sequentially.","Flush the outbox more frequently so batches stay small.","Inspect the outbox for abnormally large ops (e.g. huge content payloads) and trim them at the source.","If legitimate payloads need more room, raise MAX_BODY_BYTES consistently on client and hub."],"exampleFix":"// before\nawait client.pushOps(allOps);\n// after\nfor (const chunk of chunkOps(allOps, MAX_OPS_PER_BATCH)) {\n  await client.pushOps(chunk);\n}","handlingStrategy":"validation","validationCode":"const body = JSON.stringify({ protocol_version: 2, ops });\nif (Buffer.byteLength(body, 'utf8') <= 4 * 1024 * 1024) await client.pushOps(ops);","typeGuard":null,"tryCatchPattern":"try { await client.pushOps(ops); } catch (e) { if (e.message.includes('exceeds') && e.message.includes('bytes')) { /* split batch and retry */ } }","preventionTips":["Chunk op batches to a conservative size before pushing.","Drain the outbox on a short interval so batches never balloon.","Monitor outbox size and alert when it grows unbounded."],"tags":["sync","http","payload-size"],"backgroundTag":"payload-too-large","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"}