{
public abstract void Dispose();
- // Read request Headers and optionally request body as well.
+ /// <summary>Read request Headers and optionally request body as well.</summary>
public abstract Task<HttpRequestData> ReadRequestDataAsync(bool readBody = true);
- // Read complete request body if not done by ReadRequestData.
+ /// <summary>Read complete request body if not done by ReadRequestData.</summary>
public abstract Task<Byte[]> ReadRequestBodyAsync();
- // Sends Response back with provided statusCode, headers and content. Can be called multiple times on same response if isFinal was set to false before.
+ /// <summary>Sends Response back with provided statusCode, headers and content. Can be called multiple times on same response if isFinal was set to false before.</summary>
public abstract Task SendResponseAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string body = null, bool isFinal = true, int requestId = 0);
- // Sends Response body after SendResponse was called with isFinal: false.
+ /// <summary>Sends response headers.</summary>
+ public abstract Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0);
+ /// <summary>Sends Response body after SendResponse was called with isFinal: false.</summary>
public abstract Task SendResponseBodyAsync(byte[] data, bool isFinal = true, int requestId = 0);
- // Helper function to make it easier to convert old test with strings.
+ /// <summary>Waits for the client to signal cancellation.</summary>
+ public abstract Task WaitForCancellationAsync(bool ignoreIncomingData = true, int requestId = 0);
+
+ /// <summary>Helper function to make it easier to convert old test with strings.</summary>
public async Task SendResponseBodyAsync(string data, bool isFinal = true, int requestId = 0)
{
await SendResponseBodyAsync(String.IsNullOrEmpty(data) ? new byte[0] : Encoding.ASCII.GetBytes(data), isFinal, requestId);
using System.Collections.Generic;
using System.IO;
+using System.Net.Http.Functional.Tests;
using System.Net.Security;
using System.Net.Sockets;
-using System.Security.Authentication;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
return SendResponseHeadersAsync(streamId, endStream : isFinal, statusCode, isTrailingHeader : false, endHeaders : endHeaders, headers);
}
+ public override Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+ {
+ int streamId = requestId == 0 ? _lastStreamId : requestId;
+ return SendResponseHeadersAsync(streamId, endStream: false, statusCode, isTrailingHeader: false, endHeaders: true, headers);
+ }
+
public override Task SendResponseBodyAsync(byte[] body, bool isFinal = true, int requestId = 0)
{
int streamId = requestId == 0 ? _lastStreamId : requestId;
return SendResponseBodyAsync(streamId, body, isFinal);
}
+
+ public override async Task WaitForCancellationAsync(bool ignoreIncomingData = true, int requestId = 0)
+ {
+ int streamId = requestId == 0 ? _lastStreamId : requestId;
+
+ Frame frame;
+ do
+ {
+ frame = await ReadFrameAsync(TimeSpan.FromMilliseconds(TestHelper.PassingTestTimeoutMilliseconds));
+ Assert.NotNull(frame); // We should get Rst before closing connection.
+ Assert.Equal(0, (int)(frame.Flags & FrameFlags.EndStream));
+ if (ignoreIncomingData)
+ {
+ Assert.True(frame.Type == FrameType.Data || frame.Type == FrameType.RstStream, $"Expected Data or RstStream, got {frame.Type}");
+ }
+ else
+ {
+ Assert.Equal(FrameType.RstStream, frame.Type);
+ }
+ } while (frame.Type != FrameType.RstStream);
+
+ Assert.Equal(streamId, frame.StreamId);
+ }
}
}
return totalLength;
}
- public async Task<int> ReadBlockAsync(char[] result, int offset, int size)
+ public async Task<int> ReadBlockAsync(char[] result, int offset, int size)
{
byte[] buffer = new byte[size];
int readLength = await ReadBlockAsync(buffer, 0, size).ConfigureAwait(false);
for (int i = 0; i < readLength; i++)
{
- result[offset + i ] = asString[i];
+ result[offset + i] = asString[i];
}
return readLength;
{
bytesRead = await ReadAsync(buffer, offset, buffer.Length - offset).ConfigureAwait(false);
totalLength += bytesRead;
- offset+=bytesRead;
+ offset += bytesRead;
if (bytesRead == buffer.Length)
{
}
_readEnd += bytesRead;
- }
+ }
index = Array.IndexOf(_readBuffer, (byte)'\n', startSearch, _readEnd - startSearch);
if (index == -1)
int chunkLength = int.Parse(chunkHeader, System.Globalization.NumberStyles.HexNumber);
if (chunkLength == 0)
{
- // Last chunk. Read CRLF and exit.
+ // Last chunk. Read CRLF and exit.
await ReadLineAsync().ConfigureAwait(false);
break;
}
if (content != null || isFinal)
{
- headerString = GetHttpResponseHeaders(statusCode, headerString, contentLength, connectionClose : true);
+ headerString = GetHttpResponseHeaders(statusCode, headerString, contentLength, connectionClose: true);
}
await SendResponseAsync(headerString).ConfigureAwait(false);
await SendResponseAsync(content).ConfigureAwait(false);
}
+ public override async Task SendResponseHeadersAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, int requestId = 0)
+ {
+ string headerString = null;
+
+ if (headers != null)
+ {
+ foreach (HttpHeaderData headerData in headers)
+ {
+ headerString = headerString + $"{headerData.Name}: {headerData.Value}\r\n";
+ }
+ }
+
+ headerString = GetHttpResponseHeaders(statusCode, headerString, 0, connectionClose: true);
+
+ await SendResponseAsync(headerString).ConfigureAwait(false);
+ }
+
public override async Task SendResponseBodyAsync(byte[] body, bool isFinal = true, int requestId = 0)
{
await SendResponseAsync(Encoding.UTF8.GetString(body)).ConfigureAwait(false);
}
+
+ public override async Task WaitForCancellationAsync(bool ignoreIncomingData = true, int requestId = 0)
+ {
+ var buffer = new byte[1024];
+ while (true)
+ {
+ int bytesRead = await ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false);
+
+ if (!ignoreIncomingData)
+ {
+ Assert.Equal(0, bytesRead);
+ }
+
+ if (bytesRead == 0)
+ {
+ break;
+ }
+ }
+ }
}
public override async Task<HttpRequestData> HandleRequestAsync(HttpStatusCode statusCode = HttpStatusCode.OK, IList<HttpHeaderData> headers = null, string content = null)
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Net.Security;
{
internal static class ConnectHelper
{
- /// <summary>Pool of event args to use to establish connections.</summary>
- private static readonly ConcurrentQueueSegment<ConnectEventArgs> s_connectEventArgs =
- new ConcurrentQueueSegment<ConnectEventArgs>(
- ConcurrentQueueSegment<ConnectEventArgs>.RoundUpToPowerOf2(Math.Max(2, Environment.ProcessorCount)));
-
/// <summary>
/// Helper type used by HttpClientHandler when wrapping SocketsHttpHandler to map its
/// certificate validation callback to the one used by SslStream.
{
// Rather than creating a new Socket and calling ConnectAsync on it, we use the static
// Socket.ConnectAsync with a SocketAsyncEventArgs, as we can then use Socket.CancelConnectAsync
- // to cancel it if needed. Rent or allocate one.
- ConnectEventArgs saea;
- if (!s_connectEventArgs.TryDequeue(out saea))
- {
- saea = new ConnectEventArgs();
- }
-
+ // to cancel it if needed.
+ var saea = new ConnectEventArgs();
try
{
saea.Initialize(cancellationToken);
}
finally
{
- // Pool the event args, or if the pool is full, dispose of it.
- saea.Clear();
- if (!s_connectEventArgs.TryEnqueue(saea))
- {
- saea.Dispose();
- }
+ saea.Dispose();
}
}
Builder = b;
}
- public void Clear() => CancellationToken = default;
-
protected override void OnCompleted(SocketAsyncEventArgs _)
{
switch (SocketError)
// Wait for the response headers to complete if they haven't already, propagating any exceptions.
await responseHeadersTask.ConfigureAwait(false);
+
+ return http2Stream.GetAndClearResponse();
}
catch (Exception e)
{
}
throw;
}
-
- return http2Stream.Response;
}
private Http2Stream AddStream(HttpRequestMessage request)
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http.Headers;
private readonly CreditManager _streamWindow;
private readonly HttpRequestMessage _request;
private HttpResponseMessage _response;
+ /// <summary>Stores any trailers received after returning the response content to the caller.</summary>
+ private List<KeyValuePair<HeaderDescriptor, string>> _trailers;
private ArrayBuffer _responseBuffer; // mutable struct, do not make this readonly
private int _pendingWindowUpdate;
private object SyncObject => _streamWindow;
public int StreamId => _streamId;
- public HttpRequestMessage Request => _request;
- public HttpResponseMessage Response => _response;
+
+ public HttpResponseMessage GetAndClearResponse()
+ {
+ // Once SendAsync completes, the Http2Stream should no longer hold onto the response message.
+ // Since the Http2Stream is rooted by the Http2Connection dictionary, doing so would prevent
+ // the response stream from being collected and finalized if it were to be dropped without
+ // being disposed first.
+ HttpResponseMessage r = _response;
+ _response = null;
+ return r;
+ }
public async Task SendRequestBodyAsync(CancellationToken cancellationToken)
{
{
_state = StreamState.ExpectingHeaders;
// If we tried 100-Continue and got rejected signal that we should not send request body.
- _shouldSendRequestBody = (int)Response.StatusCode < 300;
+ _shouldSendRequestBody = (int)_response.StatusCode < 300;
shouldSendRequestBodyWaiter?.TrySetResult(_shouldSendRequestBody);
}
}
// if the header can't be added, we silently drop it.
if (_state == StreamState.ExpectingTrailingHeaders)
{
- _response.TrailingHeaders.TryAddWithoutValidation(descriptor.HeaderType == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
+ Debug.Assert(_trailers != null);
+ _trailers.Add(KeyValuePair.Create(descriptor.HeaderType == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue));
}
else if (descriptor.HeaderType == HttpHeaderType.Content)
{
+ Debug.Assert(_response != null);
_response.Content.Headers.TryAddWithoutValidation(descriptor, headerValue);
}
else
{
+ Debug.Assert(_response != null);
_response.Headers.TryAddWithoutValidation(descriptor.HeaderType == HttpHeaderType.Request ? descriptor.AsCustomHeader() : descriptor, headerValue);
}
}
if (_state == StreamState.ExpectingData)
{
_state = StreamState.ExpectingTrailingHeaders;
+ _trailers ??= new List<KeyValuePair<HeaderDescriptor, string>>();
}
}
}
}
// Start to process the response body.
- ((HttpConnectionResponseContent)_response.Content).SetStream(emptyResponse ?
- EmptyReadStream.Instance :
- (Stream)new Http2ReadStream(this));
+ var responseContent = (HttpConnectionResponseContent)_response.Content;
+ if (emptyResponse)
+ {
+ // If there are any trailers, copy them over to the response. Normally this would be handled by
+ // the response stream hitting EOF, but if there is no response body, we do it here.
+ CopyTrailersToResponseMessage(_response);
+ responseContent.SetStream(EmptyReadStream.Instance);
+ }
+ else
+ {
+ responseContent.SetStream(new Http2ReadStream(this));
+ }
// Process Set-Cookie headers.
if (_connection._pool.Settings._useCookies)
}
}
- public int ReadData(Span<byte> buffer, CancellationToken cancellationToken)
+ public int ReadData(Span<byte> buffer, HttpResponseMessage responseMessage)
{
if (buffer.Length == 0)
{
{
// Synchronously block waiting for data to be produced.
Debug.Assert(bytesRead == 0);
- GetWaiterTask(cancellationToken).AsTask().GetAwaiter().GetResult();
- CancellationHelper.ThrowIfCancellationRequested(cancellationToken);
+ GetWaiterTask(default).AsTask().GetAwaiter().GetResult();
(wait, bytesRead) = TryReadFromBuffer(buffer);
Debug.Assert(!wait);
}
if (bytesRead != 0)
{
ExtendWindow(bytesRead);
- _connection.ExtendWindow(bytesRead);
+ }
+ else
+ {
+ // We've hit EOF. Pull in from the Http2Stream any trailers that were temporarily stored there.
+ CopyTrailersToResponseMessage(responseMessage);
}
return bytesRead;
}
- public async ValueTask<int> ReadDataAsync(Memory<byte> buffer, CancellationToken cancellationToken)
+ public async ValueTask<int> ReadDataAsync(Memory<byte> buffer, HttpResponseMessage responseMessage, CancellationToken cancellationToken)
{
if (buffer.Length == 0)
{
{
ExtendWindow(bytesRead);
}
+ else
+ {
+ // We've hit EOF. Pull in from the Http2Stream any trailers that were temporarily stored there.
+ CopyTrailersToResponseMessage(responseMessage);
+ }
return bytesRead;
}
+ private void CopyTrailersToResponseMessage(HttpResponseMessage responseMessage)
+ {
+ if (_trailers != null && _trailers.Count > 0)
+ {
+ foreach (KeyValuePair<HeaderDescriptor, string> trailer in _trailers)
+ {
+ responseMessage.TrailingHeaders.TryAddWithoutValidation(trailer.Key, trailer.Value);
+ }
+ _trailers.Clear();
+ }
+ }
+
private async Task SendDataAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
ReadOnlyMemory<byte> remaining = buffer;
private sealed class Http2ReadStream : HttpBaseStream
{
private Http2Stream _http2Stream;
+ private readonly HttpResponseMessage _responseMessage;
public Http2ReadStream(Http2Stream http2Stream)
{
Debug.Assert(http2Stream != null);
+ Debug.Assert(http2Stream._response != null);
_http2Stream = http2Stream;
+ _responseMessage = _http2Stream._response;
+ }
+
+ ~Http2ReadStream()
+ {
+ if (NetEventSource.IsEnabled) _http2Stream?.Trace("");
+ try
+ {
+ Dispose(disposing: false);
+ }
+ catch (Exception e)
+ {
+ if (NetEventSource.IsEnabled) _http2Stream?.Trace($"Error: {e}");
+ }
}
protected override void Dispose(bool disposing)
return;
}
- if (disposing)
- {
- if (http2Stream._state != StreamState.Aborted && http2Stream._state != StreamState.Complete)
- {
- // If we abort response stream before endOfStream, let server know.
- IgnoreExceptions(http2Stream._connection.SendRstStreamAsync(http2Stream._streamId, Http2ProtocolErrorCode.Cancel));
- }
+ // Technically we shouldn't be doing the following work when disposing == false,
+ // as the following work relies on other finalizable objects. But given the HTTP/2
+ // protocol, we have little choice: if someone drops the Http2ReadStream without
+ // disposing of it, we need to a) signal to the server that the stream is being
+ // canceled, and b) clean up the associated state in the Http2Connection.
- http2Stream.Dispose();
+ if (http2Stream._state != StreamState.Aborted && http2Stream._state != StreamState.Complete)
+ {
+ // If we abort response stream before endOfStream, let server know.
+ IgnoreExceptions(http2Stream._connection.SendRstStreamAsync(http2Stream._streamId, Http2ProtocolErrorCode.Cancel));
}
+ http2Stream.Dispose();
+
base.Dispose(disposing);
}
throw new IOException(SR.net_http_client_execution_error, http2Stream._abortException);
}
- return http2Stream.ReadData(destination, CancellationToken.None);
+ return http2Stream.ReadData(destination, _responseMessage);
}
public override ValueTask<int> ReadAsync(Memory<byte> destination, CancellationToken cancellationToken)
return new ValueTask<int>(Task.FromCanceled<int>(cancellationToken));
}
- return http2Stream.ReadDataAsync(destination, cancellationToken);
+ return http2Stream.ReadDataAsync(destination, _responseMessage, cancellationToken);
}
public override void Write(ReadOnlySpan<byte> buffer) => throw new NotSupportedException(SR.net_http_content_readonly_stream);
--- /dev/null
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Net.Test.Common;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace System.Net.Http.Functional.Tests
+{
+ public abstract class HttpClientHandler_Finalization_Test : HttpClientHandlerTestBase
+ {
+ public HttpClientHandler_Finalization_Test(ITestOutputHelper output) : base(output) { }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private static Task GetAndDropResponse(HttpClient client, Uri url)
+ {
+ return Task.Run(async () =>
+ {
+ // Get the response stream, but don't dispose it or return it. Just drop it.
+ await client.GetStreamAsync(url);
+ });
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]
+ public async Task IncompleteResponseStream_ResponseDropped_CancelsRequestToServer()
+ {
+ using (HttpClient client = CreateHttpClient())
+ {
+ bool stopGCs = false;
+ await LoopbackServerFactory.CreateClientAndServerAsync(async url =>
+ {
+ await GetAndDropResponse(client, url);
+
+ while (!Volatile.Read(ref stopGCs))
+ {
+ await Task.Delay(10);
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ }
+ },
+ server => server.AcceptConnectionAsync(async connection =>
+ {
+ try
+ {
+ HttpRequestData data = await connection.ReadRequestDataAsync(readBody: false);
+ await connection.SendResponseHeadersAsync(headers: new HttpHeaderData[] { new HttpHeaderData("SomeHeaderName", "AndValue") });
+ await connection.WaitForCancellationAsync();
+ }
+ finally
+ {
+ Volatile.Write(ref stopGCs, true);
+ }
+ }));
+ }
+ }
+ }
+}
protected override bool UseSocketsHttpHandler => true;
}
+ public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http11_Test : HttpClientHandler_Finalization_Test
+ {
+ public SocketsHttpHandler_HttpClientHandler_Finalization_Http11_Test(ITestOutputHelper output) : base(output) { }
+ protected override bool UseSocketsHttpHandler => true;
+ }
+
+ public sealed class SocketsHttpHandler_HttpClientHandler_Finalization_Http2_Test : HttpClientHandler_Finalization_Test
+ {
+ public SocketsHttpHandler_HttpClientHandler_Finalization_Http2_Test(ITestOutputHelper output) : base(output) { }
+ protected override bool UseSocketsHttpHandler => true;
+ protected override bool UseHttp2 => true;
+ }
+
public sealed class SocketsHttpHandler_HttpClientHandler_MaxConnectionsPerServer_Test : HttpClientHandler_MaxConnectionsPerServer_Test
{
protected override bool UseSocketsHttpHandler => true;
<Compile Include="FakeDiagnosticSourceListenerObserver.cs" />
<Compile Include="FormUrlEncodedContentTest.cs" />
<Compile Include="ByteAtATimeContent.cs" />
+ <Compile Include="HttpClientHandlerTest.cs" />
+ <Compile Include="HttpClientHandlerTest.Asynchrony.cs" />
<Compile Include="HttpClientHandlerTest.Authentication.cs" />
<Compile Include="HttpClientHandlerTest.AutoRedirect.cs" />
- <Compile Include="HttpClientHandlerTest.cs" />
<Compile Include="HttpClientHandlerTest.Cancellation.cs" />
<Compile Include="HttpClientHandlerTest.ClientCertificates.cs" />
- <Compile Include="HttpClientHandlerTest.Asynchrony.cs" />
+ <Compile Include="HttpClientHandlerTest.Cookies.cs" />
<Compile Include="HttpClientHandlerTest.DefaultProxyCredentials.cs" />
- <Compile Include="HttpClientHandlerTest.Proxy.cs" />
- <Compile Include="HttpClientHandlerTest.ResponseDrain.cs" />
+ <Compile Include="HttpClientHandlerTest.Finalization.cs" />
+ <Compile Include="HttpClientHandlerTest.Headers.cs" />
<Compile Include="HttpClientHandlerTest.MaxConnectionsPerServer.cs" />
<Compile Include="HttpClientHandlerTest.MaxResponseHeadersLength.cs" />
+ <Compile Include="HttpClientHandlerTest.Proxy.cs" />
+ <Compile Include="HttpClientHandlerTest.ResponseDrain.cs" />
<Compile Include="HttpClientHandlerTest.ServerCertificates.cs" />
<Compile Include="HttpClientHandlerTest.ServerCertificates.Unix.cs" Condition="'$(TargetsUnix)' == 'true'" />
<Compile Include="HttpClientHandlerTest.ServerCertificates.Windows.cs" Condition="'$(TargetsWindows)' == 'true'" />
<Compile Include="HttpContentTest.cs" />
<Compile Include="HttpMessageInvokerTest.cs" />
<Compile Include="HttpMethodTest.cs" />
- <Compile Include="HttpClientHandlerTest.Cookies.cs" />
- <Compile Include="HttpClientHandlerTest.Headers.cs" />
<Compile Include="HttpRetryProtocolTests.cs" />
<Compile Include="IdnaProtocolTests.cs" />
<Compile Include="HttpProtocolTests.cs" />