streamable client: Connect blocks far past its context deadline against an unresponsive server (detached-context cleanup)

未关闭
#1,189 13 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

@arpankernel 已经在做这个了。

开始于 2026年9月5日。

  • #1241 来自 @arpankernel —— 未关闭

评估

难度
4/5
预计耗时
3-5 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
基本清楚
活跃度
活跃
技术栈
go
领域
api, backend

调研方向

从 mcp/streamable.go 中第 2045 行附近的 StreamableClientTransport.Connect 实现开始,然后跟踪初始化失败后的清理过程。运行提供的复现测试 TestConnectOvershootsDeadline 以观察延迟。当服务器接受连接但始终不响应时,Connect 仍能大致在调用方的 deadline 前后返回,并且为该行为提供回归覆盖,即视为完成。

由索引模型根据 Issue 内容生成。

描述

needs investigation
What

Client.Connect with a context deadline can block far past that deadline against a server that accepts TCP connections but never responds (black-holed peer). Observed: a 2s deadline returns after ~12s with the default HTTP client in a local test; in production, with connections crossing a NAT that silently dropped packets, a 10s deadline stretched to ~125s (kernel TCP timeout).

The caller's deadline bounds the initialize request itself, but not the error-path cleanup that follows. StreamableClientTransport.Connect detaches the incoming context for the connection's lifecycle (connCtx, cancel := context.WithCancel(xcontext.Detach(ctx)), mcp/streamable.go ~L2045 in v1.7.0). That detachment is intentional and documented for keeping the standalone SSE stream alive past a short connect deadline. However, when initialize fails at the deadline, the cleanup work that runs before Connect returns (the notifications/cancelled write for the abandoned call and session.Close()) issues HTTP requests bound to the detached connection context, not to the caller's expired one. Against a black-holed server those requests block until the HTTP transport or the kernel gives up, so Connect overshoots its deadline by minutes with a default http.Client (no ResponseHeaderTimeout).

Related: #1183 covers the same detached-context ordering problem for Close() specifically. This report is about Connect not honoring its own deadline on the error path.

Repro
func TestConnectOvershootsDeadline(t *testing.T) {
	// Accepts TCP connections, never responds to any request.
	ln, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		t.Fatal(err)
	}
	defer ln.Close()
	go func() {
		for {
			conn, err := ln.Accept()
			if err != nil {
				return
			}
			defer conn.Close()
		}
	}()

	client := mcp.NewClient(&mcp.Implementation{Name: "c", Version: "0"}, nil)
	tr := &mcp.StreamableClientTransport{Endpoint: "http://" + ln.Addr().String()}
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	start := time.Now()
	_, err = client.Connect(ctx, tr, nil)
	elapsed := time.Since(start)
	t.Logf("Connect returned after %s, err=%v", elapsed, err)
	if elapsed > 4*time.Second {
		t.Fatalf("Connect blocked %s past a 2s deadline", elapsed)
	}
}

With go-sdk v1.7.0 and Go 1.24 this fails: Connect returned after 12.03s, err=context deadline exceeded; sending "notifications/cancelled": rejected by transport: Post "http://127.0.0.1:...": context deadline exceeded.

Expected

Connect(ctx, ...) returns within (approximately) ctx's deadline regardless of server behavior. Error-path cleanup for a session that never initialized could be best-effort/asynchronous, or bounded by the caller's context rather than the detached connection context.

Workaround

We now run Connect+ListTools in a goroutine and select on the deadline, abandoning the goroutine on timeout, plus a cloned http.Transport with ResponseHeaderTimeout and a short dial timeout (https://github.com/coder/coder/pull/28400).

🤖 Filed by Mux (AI agent) on behalf of @ibetitsmike.

主要语言
Go
星标
5.3k
派生
607
平均合并
1 天 17 小时
30 天内合并 PR
80

环境准备

在 Codespaces 中打开

在浏览器里用你自己的 GitHub 账号启动这个项目的开发容器。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

modelcontextprotocol/go-sdk 的其他 Issue

查看 modelcontextprotocol/go-sdk 的全部 Issue

相似的 Issue

更多 Go Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。