Fix: swallow _receiveTask exceptions in SseClientSessionTransport.CloseAsync (#1432)

Co-authored-by: Xue Cai <xuecai@microsoft.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jeff Handley <jeffhandley@users.noreply.github.com>
This commit is contained in:
xue-cai 2026-07-26 11:57:27 -07:00 • committed by GitHub
commit ec216b5202
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 55 additions and 2 deletions

View file

@ -66,7 +66,7 @@ internal sealed partial class SseClientSessionTransport : TransportBase
{
LogTransportConnectFailed(Name, ex);
await CloseAsync().ConfigureAwait(false);
throw new IOException("Failed to connect transport.", ex);
throw;
}
}
@ -198,7 +198,6 @@ internal sealed partial class SseClientSessionTransport : TransportBase
LogTransportReadMessagesFailed(Name, ex);
_connectionEstablished.TrySetException(ex);
throw;
}
}
finally

View file

@ -49,6 +49,60 @@ public class HttpClientTransportAutoDetectTests(ITestOutputHelper testOutputHelp
Assert.NotNull(session);
}
[Fact]
public async Task AutoDetectMode_WhenBothTransportsFail_PreservesStreamableHttpException()
{
// Regression test: when Streamable HTTP POST fails (e.g. 403) and the SSE GET
// fallback also fails (e.g. 405), the original Streamable HTTP error should
// be preserved. The SSE connection failure is available as its inner exception.
var options = new HttpClientTransportOptions
{
Endpoint = new Uri("http://localhost"),
TransportMode = HttpTransportMode.AutoDetect,
Name = "AutoDetect test client"
};
using var mockHttpHandler = new MockHttpHandler();
using var httpClient = new HttpClient(mockHttpHandler);
await using var transport = new HttpClientTransport(options, httpClient, LoggerFactory);
mockHttpHandler.RequestHandler = (request) =>
{
if (request.Method == HttpMethod.Post)
{
// Streamable HTTP POST fails with 403 (auth error)
return Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.Forbidden,
Content = new StringContent("Forbidden")
});
}
if (request.Method == HttpMethod.Get)
{
// SSE GET fallback fails with 405
return Task.FromResult(new HttpResponseMessage
{
StatusCode = HttpStatusCode.MethodNotAllowed,
Content = new StringContent("Method Not Allowed")
});
}
throw new InvalidOperationException($"Unexpected request: {request.Method}");
};
// ConnectAsync for AutoDetect mode just creates the transport without sending
// any HTTP request. The auto-detection is triggered lazily by the first
// SendMessageAsync call, which happens inside McpClient.CreateAsync when it
// sends the JSON-RPC "initialize" message.
var ex = await Assert.ThrowsAsync<HttpRequestException>(
() => McpClient.CreateAsync(transport, cancellationToken: TestContext.Current.CancellationToken));
Assert.Contains("403", ex.Message);
Assert.IsType<HttpRequestException>(ex.InnerException);
Assert.Contains("405", ex.InnerException.Message);
}
[Fact]
public async Task AutoDetectMode_FallsBackToSse_WhenStreamableHttpFails()
{