}
// Wait for the client to close the connection, e.g. after the HttpClient is disposed.
- public async Task WaitForClientDisconnectAsync()
+ public async Task WaitForClientDisconnectAsync(bool ignoreUnexpectedFrames = false)
{
- Frame frame = await ReadFrameAsync(Timeout);
- Assert.Null(frame);
- }
-
- public void ShutdownSend()
- {
- _connectionSocket.Shutdown(SocketShutdown.Send);
- }
-
- // This will wait for the client to close the connection,
- // and ignore any meaningless frames -- i.e. WINDOW_UPDATE or expected SETTINGS ACK --
- // that we see while waiting for the client to close.
- // Only call this after sending a GOAWAY.
- public async Task WaitForConnectionShutdownAsync(bool ignoreUnexpectedFrames = false)
- {
- // Shutdown our send side, so the client knows there won't be any more frames coming.
- ShutdownSend();
-
IgnoreWindowUpdates();
+
Frame frame = await ReadFrameAsync(Timeout).ConfigureAwait(false);
if (frame != null)
{
if (!ignoreUnexpectedFrames)
{
- throw new Exception($"Unexpected frame received while waiting for client shutdown: {frame}");
+ throw new Exception($"Unexpected frame received while waiting for client disconnect: {frame}");
}
}
_ignoreWindowUpdates = false;
}
+ public void ShutdownSend()
+ {
+ _connectionSocket.Shutdown(SocketShutdown.Send);
+ }
+
+ // This will cause a server-initiated shutdown of the connection.
+ // For normal operation, you should send a GOAWAY and complete any remaining streams
+ // before calling this method.
+ public async Task WaitForConnectionShutdownAsync(bool ignoreUnexpectedFrames = false)
+ {
+ // Shutdown our send side, so the client knows there won't be any more frames coming.
+ ShutdownSend();
+
+ await WaitForClientDisconnectAsync(ignoreUnexpectedFrames: ignoreUnexpectedFrames);
+ }
+
// This is similar to WaitForConnectionShutdownAsync but will send GOAWAY for you
// and will ignore any errors if client has already shutdown
public async Task ShutdownIgnoringErrorsAsync(int lastStreamId, ProtocolErrors errorCode = ProtocolErrors.NO_ERROR)
private Exception _resetException;
private bool _canRetry; // if _resetException != null, this indicates the stream was refused and so the request is retryable
+ // This flag indicates that, per section 8.1 of the RFC, the server completed the response and then sent a RST_STREAM with error = NO_ERROR.
+ // This is a signal to stop sending the request body, but the request is still considered successful.
+ private bool _requestBodyAbandoned;
+
/// <summary>
/// The core logic for the IValueTaskSource implementation.
///
{
// Create a TCS for handling Expect: 100-continue semantics. See WaitFor100ContinueAsync.
// Note we need to create this in the constructor, because we can receive a 100 Continue response at any time after the constructor finishes.
- _expect100ContinueWaiter = new TaskCompletionSource<bool>(TaskContinuationOptions.RunContinuationsAsynchronously);
+ _expect100ContinueWaiter = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
}
}
{
if (NetEventSource.IsEnabled) Trace($"Failed to send request body: {e}");
- // Cancel the stream before we set _requestCompletionState below.
- // Otherwise, a response stream reader may race with the actual Cancel.
- Cancel();
+ bool signalWaiter = false;
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
Debug.Assert(_requestCompletionState == StreamCompletionState.InProgress, $"Request already completed with state={_requestCompletionState}");
- _requestCompletionState = StreamCompletionState.Failed;
+ if (_requestBodyAbandoned)
+ {
+ // See comments on _requestBodyAbandoned.
+ // In this case, the request is still considered successful and we do not want to send a RST_STREAM,
+ // and we also don't want to propagate any error to the caller, in particular for non-duplex scenarios.
+ Debug.Assert(_responseCompletionState == StreamCompletionState.Completed);
+ _requestCompletionState = StreamCompletionState.Completed;
+ Complete();
+ return;
+ }
- // Cancel above should ensure that the response is either Completed or Failed now.
- Debug.Assert(_responseCompletionState != StreamCompletionState.InProgress);
+ // This should not cause RST_STREAM to be sent because the request is still marked as in progress.
+ signalWaiter = CancelResponseBody();
+ _requestCompletionState = StreamCompletionState.Failed;
Reset();
}
+ if (signalWaiter)
+ {
+ _waitSource.SetResult(true);
+ }
+
throw;
}
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
Debug.Assert(_requestCompletionState == StreamCompletionState.InProgress, $"Request already completed with state={_requestCompletionState}");
CancellationTokenSource requestBodyCancellationSource = null;
bool signalWaiter = false;
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
if (_requestCompletionState == StreamCompletionState.InProgress)
Debug.Assert(requestBodyCancellationSource != null);
}
- if (_responseCompletionState == StreamCompletionState.InProgress)
- {
- _responseCompletionState = StreamCompletionState.Failed;
- if (_requestCompletionState != StreamCompletionState.InProgress)
- {
- Reset();
- }
- }
-
- // Discard any remaining buffered response data
- if (_responseBuffer.ActiveLength != 0)
- {
- _responseBuffer.Discard(_responseBuffer.ActiveLength);
- }
-
- _responseProtocolState = ResponseProtocolState.Aborted;
-
- signalWaiter = _hasWaiter;
- _hasWaiter = false;
+ signalWaiter = CancelResponseBody();
}
if (requestBodyCancellationSource != null)
}
}
+ // Returns whether the waiter should be signalled or not.
+ private bool CancelResponseBody()
+ {
+ Debug.Assert(Monitor.IsEntered(SyncObject));
+
+ if (_responseCompletionState == StreamCompletionState.InProgress)
+ {
+ _responseCompletionState = StreamCompletionState.Failed;
+ if (_requestCompletionState != StreamCompletionState.InProgress)
+ {
+ Reset();
+ }
+ }
+
+ // Discard any remaining buffered response data
+ if (_responseBuffer.ActiveLength != 0)
+ {
+ _responseBuffer.Discard(_responseBuffer.ActiveLength);
+ }
+
+ _responseProtocolState = ResponseProtocolState.Aborted;
+
+ bool signalWaiter = _hasWaiter;
+ _hasWaiter = false;
+
+ return signalWaiter;
+ }
+
public void OnWindowUpdate(int amount) => _streamWindow.AdjustCredit(amount);
public void OnResponseHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
// TODO: ISSUE 31309: Optimize HPACK static table decoding
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
if (_responseProtocolState == ResponseProtocolState.Aborted)
public void OnResponseHeadersStart()
{
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
if (_responseProtocolState == ResponseProtocolState.Aborted)
public void OnResponseHeadersComplete(bool endStream)
{
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
bool signalWaiter;
lock (SyncObject)
{
public void OnResponseData(ReadOnlySpan<byte> buffer, bool endStream)
{
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
bool signalWaiter;
lock (SyncObject)
{
}
}
- public void OnReset(Exception resetException, bool canRetry = false)
+ // This is called in several different cases:
+ // (1) Receiving RST_STREAM on this stream. If so, the resetStreamErrorCode will be non-null, and canRetry will be true only if the error code was REFUSED_STREAM.
+ // (2) Receiving GOAWAY that indicates this stream has not been processed. If so, canRetry will be true.
+ // (3) Connection IO failure or protocol violation. If so, resetException will contain the relevant exception and canRetry will be false.
+ // (4) Receiving EOF from the server. If so, resetException will contain an exception like "expected 9 bytes of data", and canRetry will be false.
+ public void OnReset(Exception resetException, Http2ProtocolErrorCode? resetStreamErrorCode = null, bool canRetry = false)
{
- if (NetEventSource.IsEnabled) Trace($"{nameof(resetException)}={resetException}");
+ if (NetEventSource.IsEnabled) Trace($"{nameof(resetException)}={resetException}, {nameof(resetStreamErrorCode )}={resetStreamErrorCode}");
+ bool cancel = false;
+ CancellationTokenSource requestBodyCancellationSource = null;
+
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
// If we've already finished, don't actually reset the stream.
canRetry = false;
}
- _resetException = resetException;
- _canRetry = canRetry;
+ // Per section 8.1 in the RFC:
+ // If the server has completed the response body (i.e. we've received EndStream)
+ // but the request body is still sending, and we then receive a RST_STREAM with errorCode = NO_ERROR,
+ // we treat this specially and simply cancel sending the request body, rather than treating
+ // the entire request as failed.
+ if (resetStreamErrorCode == Http2ProtocolErrorCode.NoError &&
+ _responseCompletionState == StreamCompletionState.Completed)
+ {
+ if (_requestCompletionState == StreamCompletionState.InProgress)
+ {
+ _requestBodyAbandoned = true;
+ requestBodyCancellationSource = _requestBodyCancellationSource;
+ Debug.Assert(requestBodyCancellationSource != null);
+ }
+ }
+ else
+ {
+ _resetException = resetException;
+ _canRetry = canRetry;
+ cancel = true;
+ }
}
- Cancel();
+ if (requestBodyCancellationSource != null)
+ {
+ Debug.Assert(_requestBodyAbandoned);
+ Debug.Assert(!cancel);
+ requestBodyCancellationSource.Cancel();
+ }
+ else
+ {
+ Cancel();
+ }
}
private void CheckResponseBodyState()
// Determine if we have enough data to process up to complete final response headers.
private (bool wait, bool isEmptyResponse) TryEnsureHeaders()
{
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
CheckResponseBodyState();
{
Debug.Assert(buffer.Length > 0);
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
CheckResponseBodyState();
{
// Check if the response body has been fully consumed.
bool fullyConsumed = false;
+ Debug.Assert(!Monitor.IsEntered(SyncObject));
lock (SyncObject)
{
if (_responseBuffer.ActiveLength == 0 && _responseProtocolState == ResponseProtocolState.Complete)
var thisRef = (Http2Stream)s;
bool signalWaiter;
+ Debug.Assert(!Monitor.IsEntered(thisRef.SyncObject));
lock (thisRef.SyncObject)
{
signalWaiter = thisRef._hasWaiter;
}
}
+ [Fact]
+ public async Task PostAsyncDuplex_ServerCompletesResponseBodyThenResetsStreamWithNoError_SuccessAndRequestBodyCancelled()
+ {
+ // Per section 8.1 of the RFC:
+ // Receiving RST_STREAM with NO_ERROR after receiving EndStream on the response body is a special case.
+ // We should stop sending the request body, but treat the request as successful and
+ // return the completed response body to the user.
+
+ byte[] contentBytes = Encoding.UTF8.GetBytes("Hello world");
+
+ using (var server = Http2LoopbackServer.CreateServer())
+ {
+ Http2LoopbackConnection connection;
+ using (HttpClient client = CreateHttpClient())
+ {
+ var duplexContent = new DuplexContent();
+
+ var request = new HttpRequestMessage(HttpMethod.Post, server.Address);
+ request.Version = new Version(2, 0);
+ request.Content = duplexContent;
+ Task<HttpResponseMessage> responseTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
+
+ connection = await server.EstablishConnectionAsync();
+
+ // Client should have sent the request headers, and the request stream should now be available
+ Stream requestStream = await duplexContent.WaitForStreamAsync();
+
+ // Flush the content stream. Otherwise, the request headers are not guaranteed to be sent.
+ await requestStream.FlushAsync();
+
+ (int streamId, _) = await connection.ReadAndParseRequestHeaderAsync(readBody: false);
+
+ // Send data to the server, even before we've received response headers.
+ await SendAndReceiveRequestDataAsync(contentBytes, requestStream, connection, streamId);
+
+ // Send response headers
+ await connection.SendResponseHeadersAsync(streamId, endStream: false);
+ HttpResponseMessage response = await responseTask;
+ Stream responseStream = await response.Content.ReadAsStreamAsync();
+
+ // Send response body and complete response
+ await connection.SendResponseDataAsync(streamId, contentBytes, endStream: true);
+
+ // Send RST_STREAM to client with error = NO_ERROR.
+ await connection.WriteFrameAsync(new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.NO_ERROR, streamId));
+
+ // Ensure client has processed the RST_STREAM.
+ await connection.PingPong();
+
+ // Attempting to write on the request body should now fail with OperationCanceledException.
+ Exception e = await Assert.ThrowsAnyAsync<OperationCanceledException>(async () => { await SendAndReceiveRequestDataAsync(contentBytes, requestStream, connection, streamId); });
+
+ // Propagate the exception to the request stream serialization task.
+ // This allows the request processing to complete.
+ duplexContent.Fail(e);
+
+ // We should receive the response body and EOF.
+ byte[] readBuffer = new byte[contentBytes.Length];
+ int bytesRead = await responseStream.ReadAsync(readBuffer);
+ Assert.True(contentBytes.SequenceEqual(readBuffer));
+ bytesRead = await responseStream.ReadAsync(readBuffer);
+ Assert.Equal(0, bytesRead);
+ }
+
+ // On handler dispose, client should shutdown the connection without sending additional frames.
+ await connection.WaitForClientDisconnectAsync();
+ }
+ }
+
+ [Fact]
+ public async Task PostAsyncNonDuplex_ServerCompletesResponseBodyThenResetsStreamWithNoError_SuccessAndRequestBodyCancelled()
+ {
+ // Per section 8.1 of the RFC:
+ // Receiving RST_STREAM with NO_ERROR after receiving EndStream on the response body is a special case.
+ // We should stop sending the request body, but treat the request as successful and
+ // return the completed response body to the user.
+
+ byte[] contentBytes = Encoding.UTF8.GetBytes("Hello world");
+
+ using (var server = Http2LoopbackServer.CreateServer())
+ {
+ Http2LoopbackConnection connection;
+ using (HttpClient client = CreateHttpClient())
+ {
+ // We want non-duplex content, so use ByteArrayContent,
+ // but make it large enough to ensure that the content can't be fully sent because of flow control limitations.
+ // This allows us to validate that the content is actually canceled, not just fully sent and completed.
+ const int ContentSize = 100_000;
+ var requestContent = new ByteArrayContent(TestHelper.GenerateRandomContent(ContentSize));
+
+ var request = new HttpRequestMessage(HttpMethod.Post, server.Address);
+ request.Version = new Version(2, 0);
+ request.Content = requestContent;
+ Task<HttpResponseMessage> responseTask = client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
+
+ connection = await server.EstablishConnectionAsync();
+
+ (int streamId, _) = await connection.ReadAndParseRequestHeaderAsync(readBody: false);
+
+ // Send full response
+ await connection.SendResponseHeadersAsync(streamId, endStream: false);
+ await connection.SendResponseDataAsync(streamId, contentBytes, endStream: true);
+
+ // Send RST_STREAM to client with error = NO_ERROR.
+ await connection.WriteFrameAsync(new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.NO_ERROR, streamId));
+
+ // Response should now complete successfully
+ HttpResponseMessage response = await responseTask;
+ Assert.Equal("Hello world", await response.Content.ReadAsStringAsync());
+ }
+
+ // On handler dispose, client should shutdown the connection. Ignore any request stream frames already sent.
+ await connection.WaitForClientDisconnectAsync(ignoreUnexpectedFrames: true);
+ }
+ }
+
[Theory]
[InlineData(true, HttpStatusCode.Forbidden)]
[InlineData(false, HttpStatusCode.Forbidden)]