From 717e6c77cd0a279c8116ae9a37d792e0e8f446eb Mon Sep 17 00:00:00 2001 From: "pavel.guseynov" Date: Sat, 7 Nov 2026 11:52:17 +0500 Subject: [PATCH] fix(server): keep the connection health policy of master for stream failures Master keeps a remote MCP connection healthy after a tool call timeout, and its tests check this. The event stream change marked the connection degraded after a timeout and unhealthy after a transport failure. This commit removes these two health writes, so a timeout or a peer reset fails only that call, as on master. Authorization failures still mark the connection unhealthy. The call evidence now records the response status, content type and request id before the body is read. A body that fails partway records the bytes received so far as its size. Co-Authored-By: Claude Opus 5.5 --- server/src/__tests__/tool-gateway.test.ts | 2 +- server/src/services/tool-gateway.ts | 28 +++++++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/server/src/__tests__/tool-gateway.test.ts b/server/src/__tests__/tool-gateway.test.ts index 8f73b3d9322..51562c33b3c 100644 --- a/server/src/__tests__/tool-gateway.test.ts +++ b/server/src/__tests__/tool-gateway.test.ts @@ -4629,7 +4629,7 @@ rl.on("line", (line) => { } else { const timeout = scenario === "default deadline" || scenario === "explicit deadline"; expectGatewayError(result.error, timeout ? 504 : 502, timeout ? "tool_timeout" : "mcp_remote_fetch_failed"); - expect(stored.healthStatus).toBe(timeout ? "degraded" : "error"); + expect(stored.healthStatus).toBe("ok"); expect(event.metadata).toMatchObject({ execution: { failureKind: timeout ? "invocation_timeout" : "transport_failure", deadline: { timeoutMs: scenario === "explicit deadline" ? 1500 : 30_000, aborted: timeout }, diff --git a/server/src/services/tool-gateway.ts b/server/src/services/tool-gateway.ts index 4697a60a4e7..089137875fb 100644 --- a/server/src/services/tool-gateway.ts +++ b/server/src/services/tool-gateway.ts @@ -6521,6 +6521,17 @@ export function createToolGatewayService( // automatically: the failed call may have changed app data. if (mcpSession) forgetMcpHttpSession(mcpSession); } + // Record the response headers before the body is read, so a body that + // fails partway still has its status and content type in the evidence. + execution.response = { + httpStatus: response.status, + contentType: response.headers.get("content-type"), + bodySizeBytes: 0, + upstreamRequestId: + response.headers.get("x-request-id") ?? + response.headers.get("x-zapier-request-id") ?? + response.headers.get("traceparent"), + }; const body = response.ok ? JSON.stringify(await readMcpHttpResponse(response, requestId, { maxBytes: MAX_REMOTE_MCP_RESPONSE_BYTES, @@ -6542,15 +6553,7 @@ export function createToolGatewayService( }, })) : await readBoundedRemoteResponse(response); - execution.response = { - httpStatus: response.status, - contentType: response.headers.get("content-type"), - bodySizeBytes: Buffer.byteLength(body, "utf8"), - upstreamRequestId: - response.headers.get("x-request-id") ?? - response.headers.get("x-zapier-request-id") ?? - response.headers.get("traceparent"), - }; + execution.response.bodySizeBytes = Buffer.byteLength(body, "utf8"); if (isInsufficientConnectionScope(response, body)) { await markCallHealth("degraded", INSUFFICIENT_CONNECTION_SCOPE_MESSAGE); throw new ToolGatewayHttpError(403, INSUFFICIENT_CONNECTION_SCOPE_MESSAGE, "oauth_insufficient_scope", { @@ -6653,13 +6656,10 @@ export function createToolGatewayService( } catch (error) { execution.error = remoteHttpErrorEvidence(error); execution.deadline = { timeoutMs: ms, aborted: controller.signal.aborted }; + if (execution.response && execution.stream) execution.response.bodySizeBytes = execution.stream.bytesReceived; if (controller.signal.aborted || (error instanceof Error && error.name === "AbortError")) { if (mcpSession) forgetMcpHttpSession(mcpSession); execution.failureKind = "invocation_timeout"; - await markCallHealth( - "degraded", - "Remote MCP tool call timed out; transport remains viable.", - ); throw new ToolGatewayHttpError(504, "Remote MCP tool call timed out", "tool_timeout", { connectionId: connection.id, catalogEntryId: entry.id, execution, failureKind: "invocation_timeout", }); @@ -6696,8 +6696,6 @@ export function createToolGatewayService( execution.failureKind = "transport_failure"; if (error instanceof McpHttpInitializationError && (error.status === 401 || error.status === 403)) { await markCallHealth("error", "Remote MCP session authorization failed."); - } else { - await markCallHealth("error", "Remote MCP tool call failed."); } if (mcpSession) forgetMcpHttpSession(mcpSession); throw new ToolGatewayHttpError(