{"record":{"id":"598c732e06dda8b6","repo":"can1357/oh-my-pi","slug":"notify-timeout-after-timeout-ms","errorCode":null,"errorMessage":"Notify timeout after ${timeout}ms","messagePattern":"Notify timeout after (.+?)ms","errorType":"exception","errorClass":"MCPTransportError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/mcp/transports/http.ts","lineNumber":820,"sourceCode":"\t\t\tif (contentType.includes(\"text/event-stream\") && response.body) {\n\t\t\t\t// Use the SSE connection's signal if available; otherwise keep the existing finite read timeout.\n\t\t\t\tif (this.#sseConnection) {\n\t\t\t\t\tthis.#trackBackgroundDrain(\n\t\t\t\t\t\tthis.#readSSEStream(response.body, this.#operationSignal(this.#sseConnection.signal)),\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tconst readOperation = createMCPTimeout(timeout, this.#operationSignal());\n\t\t\t\t\tconst signal = readOperation.signal ?? getNeverAbortSignal();\n\t\t\t\t\tthis.#trackBackgroundDrain(\n\t\t\t\t\t\tthis.#readSSEStream(response.body, signal).finally(() => readOperation.clear()),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tawait response.body?.cancel();\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (operation.isTimeoutAbort(error)) {\n\t\t\t\tthrow new MCPTransportError({\n\t\t\t\t\ttransport: \"http\",\n\t\t\t\t\tstage,\n\t\t\t\t\tfailure: \"timeout\",\n\t\t\t\t\tmessage: `Notify timeout after ${timeout}ms`,\n\t\t\t\t\tretryable: false,\n\t\t\t\t\ttraceId,\n\t\t\t\t\tcause: error,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (error instanceof Error && error.name === \"AbortError\") throw error;\n\t\t\tthrow normalizeMCPTransportError(error, { transport: \"http\", stage, traceId });\n\t\t} finally {\n\t\t\toperation.clear();\n\t\t}\n\t}\n\n\tclose(): Promise<void> {\n\t\tif (this.#closePromise) return this.#closePromise;","sourceCodeStart":802,"sourceCodeEnd":838,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/mcp/transports/http.ts#L802-L838","documentation":"The notification POST exceeded the configured MCP timeout. The fetch (or body read for piggybacked SSE data) was aborted by the timeout controller, detected via operation.isTimeoutAbort, and rethrown as a timeout-stage MCPTransportError that is explicitly non-retryable — notifications have no response, so a timeout cannot distinguish delivered from undelivered and a blind replay could double-apply side effects.","triggerScenarios":"Server slow to accept the POST (overloaded, GC pause, blocked handler); configured timeout lower than server processing latency; network stall between client and server; response SSE piggyback stream kept open longer than the timeout.","commonSituations":"Large notification payloads over slow links; MCP server handling a long-running tool that blocks its event loop; timeout config tuned for requests applied to a heavily loaded server; proxy queuing delays.","solutions":["Raise the MCP timeout configuration (config.timeout) if the server legitimately takes longer than the current limit.","Check server load/latency and fix the slow handler blocking the notification path.","Verify network path health (proxies, VPN) for stalls; the error includes a traceId to correlate with server logs.","Do not auto-retry this error: confirm on the server whether the notification was applied before resending, to avoid duplicate effects.","If notifications routinely time out, switch the server to return 202 immediately and process asynchronously."],"exampleFix":"// before: default/aggressive timeout\nconst transport = new HttpTransport({ url, timeout: 5000 });\n// after: headroom for slow notification acceptance\nconst transport = new HttpTransport({ url, timeout: 30000 });","handlingStrategy":"retry","validationCode":"// size check before sending a bulky notification\nconst payload = JSON.stringify({ jsonrpc: '2.0', method, params });\nif (payload.length > 1_000_000) console.warn('Notification payload large; raise timeout or split');","typeGuard":"function isNotifyTimeout(e: unknown): e is MCPTransportError {\n  return e instanceof MCPTransportError && e.failure === 'timeout' && /Notify timeout after/.test(e.message);\n}","tryCatchPattern":"try {\n  await transport.notify(method, params);\n} catch (e) {\n  if (isNotifyTimeout(e)) {\n    // delivery unknown — never blind-retry; verify server-side state first\n    logger.warn('MCP notify timed out', { traceId: e.traceId });\n  } else throw e;\n}","preventionTips":["Configure a timeout with realistic headroom for your server's p99 latency.","Keep the server event loop unblocked so notification POSTs are acknowledged fast.","Monitor network path (VPN/proxy) for stalls.","Make notification handling idempotent server-side so manual replays are safe."],"tags":["mcp","http","timeout","notification"],"backgroundTag":"request-timeout","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}