diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs index 6fde1077122f64..143021d56f7732 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/Http2Connection.cs @@ -255,8 +255,10 @@ public async ValueTask SetupAsync(CancellationToken cancellationToken) throw; } - // TODO: Review this case! - throw new IOException(SR.net_http_http2_connection_not_established, e); + // Use _abortException if available, as it contains the real reason for the connection failure. + // For example, when ProcessIncomingFramesAsync detects a server-initiated disconnect and calls Abort(), + // _abortException will have the original IOException, while 'e' here may be an uninformative ObjectDisposedException. + throw new IOException(SR.net_http_http2_connection_not_established, _abortException ?? e); } // Avoid capturing the initial request's ExecutionContext for the entire lifetime of the new connection. diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs index 416cd342435a69..36d1a46e717a6f 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpClientHandlerTest.Http2.cs @@ -687,6 +687,48 @@ public async Task GetAsync_ServerSendSettingsWithoutMaxConcurrentStreams_ClientA } } + [ConditionalTheory(nameof(SupportsAlpn))] + [InlineData(false)] // server disconnects without sending SETTINGS + [InlineData(true)] // server sends GOAWAY instead of SETTINGS + public async Task ServerDisconnectDuringSetup_PropagatesMeaningfulException(bool sendGoAway) + { + using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer()) + using (HttpClient client = CreateHttpClient()) + { + Task sendTask = client.GetAsync(server.Address); + + // Accept connection and read client preface, but do NOT send SETTINGS. + Http2LoopbackConnection connection = await server.AcceptConnectionAsync(); + + if (sendGoAway) + { + // Send GOAWAY instead of SETTINGS, then shut down. + await connection.SendGoAway(0, ProtocolErrors.ENHANCE_YOUR_CALM); + await connection.ShutdownSendAsync(); + + // The client should throw HttpRequestException(HttpProtocolError) wrapping + // HttpProtocolException with the GOAWAY error code. + await AssertProtocolErrorAsync(sendTask, ProtocolErrors.ENHANCE_YOUR_CALM); + } + else + { + // Immediately shut down the connection without sending SETTINGS. + // This simulates a server-side disconnect during HTTP/2 setup. + await connection.ShutdownSendAsync(); + + // The client should throw HttpRequestException(InvalidResponse) wrapping + // HttpIOException(InvalidResponse) -> HttpIOException(ResponseEnded), + // indicating the server disconnected before sending SETTINGS. + HttpRequestException ex = await Assert.ThrowsAsync(() => sendTask); + Assert.Equal(HttpRequestError.InvalidResponse, ex.HttpRequestError); + HttpIOException httpIoEx = Assert.IsAssignableFrom(ex.InnerException); + Assert.Equal(HttpRequestError.InvalidResponse, httpIoEx.HttpRequestError); + HttpIOException innerHttpIoEx = Assert.IsAssignableFrom(httpIoEx.InnerException); + Assert.Equal(HttpRequestError.ResponseEnded, innerHttpIoEx.HttpRequestError); + } + } + } + // This test is based on RFC 7540 section 6.1: // "If a DATA frame is received whose stream identifier field is 0x0, the recipient MUST // respond with a connection error (Section 5.4.1) of type PROTOCOL_ERROR."