Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,23 @@ private async Task CloseAsync()
{
if (_receiveTask != null)
{
await _receiveTask.ConfigureAwait(false);
// Swallow exceptions from _receiveTask so that callers (e.g. ConnectAsync's
// catch block) are not disrupted. The exception was already observed and
// forwarded via _connectionEstablished.TrySetException in ReceiveMessagesAsync.
try
{
await _receiveTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Expected during normal shutdown via _connectionCts cancellation.
}
catch (Exception)
{
// Already observed by ReceiveMessagesAsync — logged and set on
// _connectionEstablished. Swallowing here prevents the exception
// from escaping CloseAsync and preempting the caller's own throw.
}
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,56 @@ public async Task AutoDetectMode_UsesStreamableHttp_WhenServerSupportsIt()
Assert.NotNull(session);
}

[Fact]
public async Task AutoDetectMode_WhenBothTransportsFail_ThrowsInvalidOperationException()
{
// Regression test: when Streamable HTTP POST fails (e.g. 403) and the SSE GET
// fallback also fails (e.g. 405), ConnectAsync should wrap the error in an
// InvalidOperationException. Previously, CloseAsync() would re-throw the
// HttpRequestException from the faulted _receiveTask, preempting the wrapping.
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}");
};

var ex = await Assert.ThrowsAsync<InvalidOperationException>(
() => transport.ConnectAsync(TestContext.Current.CancellationToken));

Assert.Equal("Failed to connect transport", ex.Message);
Assert.IsType<HttpRequestException>(ex.InnerException);
}

[Fact]
public async Task AutoDetectMode_FallsBackToSse_WhenStreamableHttpFails()
{
Expand Down