HTTP+SSE client: POST responses are never disposed, leaking one connection per sent message
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 1/5
- Tiempo estimado
- Menos de una hora
- Aptitud para principiantes
- 88/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- csharp
- Área
- api, networking
Línea de trabajo
Comienza en src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs, en SendMessageAsync, y compara el manejo de la respuesta POST con la respuesta GET de SSE eliminada en el mismo archivo. Haz que la ruta de éxito elimine la respuesta de forma determinista y, después, verifica que las conexiones POST se devuelvan al pool y dejen de acumularse, tal como se describe en el issue.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Description
In the legacy HTTP+SSE client transport, every JSON-RPC message sent via POST leaks its HTTP connection until the GC happens to finalize the abandoned response object.
Two lines combine to cause this:
-
McpHttpClient.SendAsyncsends every request withHttpCompletionOption.ResponseHeadersRead:
https://github.com/modelcontextprotocol/csharp-sdk/blob/v0.3.0-preview.3/src/ModelContextProtocol.Core/Client/McpHttpClient.cs#L22
In this mode the underlying connection is not returned to the pool until the response content is fully consumed or theHttpResponseMessageis disposed. -
SseClientSessionTransport.SendMessageAsyncreceives that response withoutusing, and on the success path neither reads the content nor disposes it:
https://github.com/modelcontextprotocol/csharp-sdk/blob/v0.3.0-preview.3/src/ModelContextProtocol.Core/Client/SseClientSessionTransport.cs#L85
The method simply returns, leaving the response — and its connection — checked out indefinitely. It is only reclaimed when the GC finalizes the abandoned response (nondeterministic), or an idle/keep-alive timeout eventually fires.
The surrounding code suggests an oversight rather than a design choice:
- the SSE GET response is wrapped in
using var response(same file, receive loop), - the failure path does read the content (for logging) before throwing —
only the success path (the common case, a 202 Accepted) leaks. The same pattern is still present on main today (var response at SseClientSessionTransport.cs#L93 vs. using var response at #L161).
Observed impact
Measured with OS-level connection counting (netstat / IPGlobalProperties.GetActiveTcpConnections), .NET 8/10, package ModelContextProtocol 0.3.0-preview.3, against a local ModelContextProtocol.AspNetCore server:
- A single client connect +
ListToolsAsyncperforms 3 POSTs (initialize,notifications/initialized,tools/list); each leaves one ESTABLISHED connection stuck. Per client: 1 live SSE connection + 3 stuck POST connections (the app-side SSE count and the OS socket count diverge, e.g. 2 vs 11 for one round against 3 servers). - The stuck connections never return to the pool, so they are also never reused — each subsequent POST opens a fresh socket.
- Disposing the client (and the
HttpClient, viaownsHttpClient: true) does not release them: from the handler's perspective those requests are still in flight, andDisposedeliberately does not tear down in-flight connections. - Applications that create clients per operation accumulate a sawtooth of dead ESTABLISHED sockets, bounded only by GC timing / idle timeouts.
Suggested fix
In SseClientSessionTransport.SendMessageAsync:
using var response = await _httpClient.SendAsync(httpRequestMessage, message, cancellationToken).ConfigureAwait(false);
(one-word change: var → using var). With the response disposed, the connection returns to the pool deterministically and subsequent POSTs reuse a single connection instead of opening a new socket per message.
How this was found
While investigating unexpected TCP connection growth in an application that uses the legacy SSE transport: the application's own SSE bookkeeping and the OS-level socket count diverged. Ruling out server-side closes (no TIME_WAIT traces — the sockets sit in ESTABLISHED) and timing races (exactly one stuck connection per POST, on every run) pointed at undisposed responses; reading the transport source then confirmed the missing using.
- Lenguaje dominante
- C#
- Estrellas
- 4.5k
- Forks
- 814
- Merge medio
- 9 d 19 h
- PR fusionados (30 d)
- 4
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de modelcontextprotocol/csharp-sdk
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
modelcontextprotocol/csharp-sdk#1867 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 84/100
modelcontextprotocol/csharp-sdk#1836 ·
-
enhancement needs confirmation
Dificultad 2/5 1-3 horas Aptitud para principiantes 64/100
modelcontextprotocol/csharp-sdk#678 · 1 comentario ·
-
enhancement needs confirmation P3 ready for work
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
modelcontextprotocol/csharp-sdk#515 · 6 comentarios · 3 reacciones ·
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 28/100
modelcontextprotocol/csharp-sdk#1881 ·
Todos los issues de modelcontextprotocol/csharp-sdk
Issues similares
-
bug documentation frontend
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
azurenoops/spin_agent#975 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
-
:watch: Not Triaged 11.0 fundamentals/subsvc
Dificultad 2/5 1-3 horas Aptitud para principiantes 92/100
dotnet/AspNetCore.Docs#37699 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 72/100
SubtitleEdit/subtitleedit#15108 · 1 comentario ·
-
area/docs-content Bug pulumi/docs
Dificultad 1/5 1-3 horas Aptitud para principiantes 94/100