HTTP+SSE client: POST responses are never disposed, leaking one connection per sent message

Abierto Apto para principiantes
#1,840 1 comentario 0 reacciones 0 asignados Ver en GitHub

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:

  1. McpHttpClient.SendAsync sends every request with HttpCompletionOption.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 the HttpResponseMessage is disposed.

  2. SseClientSessionTransport.SendMessageAsync receives that response without using, 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 + ListToolsAsync performs 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, via ownsHttpClient: true) does not release them: from the handler's perspective those requests are still in flight, and Dispose deliberately 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: varusing 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

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de modelcontextprotocol/csharp-sdk

Todos los issues de modelcontextprotocol/csharp-sdk

Issues similares

Más issues de C#

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.