// 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()
+ public async Task WaitForConnectionShutdownAsync(bool ignoreUnexpectedFrames = false)
{
// Shutdown our send side, so the client knows there won't be any more frames coming.
ShutdownSend();
Frame frame = await ReadFrameAsync(Timeout).ConfigureAwait(false);
if (frame != null)
{
- throw new Exception($"Unexpected frame received while waiting for client shutdown: {frame}");
+ if (!ignoreUnexpectedFrames)
+ {
+ throw new Exception($"Unexpected frame received while waiting for client shutdown: {frame}");
+ }
}
_connectionStream.Close();
// 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)
+ public async Task ShutdownIgnoringErrorsAsync(int lastStreamId, ProtocolErrors errorCode = ProtocolErrors.NO_ERROR)
{
try
{
- await SendGoAway(lastStreamId).ConfigureAwait(false);
- await WaitForConnectionShutdownAsync().ConfigureAwait(false);
+ await SendGoAway(lastStreamId, errorCode).ConfigureAwait(false);
+ await WaitForConnectionShutdownAsync(ignoreUnexpectedFrames: true).ConfigureAwait(false);
}
catch (IOException)
{
return frame.StreamId;
}
+ public async Task<HeadersFrame> ReadRequestHeaderFrameAsync()
+ {
+ // Receive HEADERS frame for request.
+ Frame frame = await ReadFrameAsync(Timeout).ConfigureAwait(false);
+ if (frame == null)
+ {
+ throw new IOException("Failed to read Headers frame.");
+ }
+
+ Assert.Equal(FrameType.Headers, frame.Type);
+ Assert.Equal(FrameFlags.EndHeaders | FrameFlags.EndStream, frame.Flags);
+ return (HeadersFrame)frame;
+ }
+
private static (int bytesConsumed, int value) DecodeInteger(ReadOnlySpan<byte> headerBlock, byte prefixMask)
{
int value = headerBlock[0] & prefixMask;
return (streamId, requestData);
}
- public async Task SendGoAway(int lastStreamId)
+ public async Task SendGoAway(int lastStreamId, ProtocolErrors errorCode = ProtocolErrors.NO_ERROR)
{
- GoAwayFrame frame = new GoAwayFrame(lastStreamId, 0, new byte[] { }, 0);
+ GoAwayFrame frame = new GoAwayFrame(lastStreamId, (int)errorCode, new byte[] { }, 0);
await WriteFrameAsync(frame).ConfigureAwait(false);
}
public async Task PingPong()
{
- PingFrame ping = new PingFrame(new byte[8] { 1, 2, 3, 4, 50, 60, 70, 80 }, FrameFlags.None, 0);
+ byte[] pingData = new byte[8] { 1, 2, 3, 4, 50, 60, 70, 80 };
+ PingFrame ping = new PingFrame(pingData, FrameFlags.None, 0);
await WriteFrameAsync(ping).ConfigureAwait(false);
- Frame pingAck = await ReadFrameAsync(Timeout).ConfigureAwait(false);
- if (pingAck.Type != FrameType.Ping || !pingAck.AckFlag)
+ PingFrame pingAck = (PingFrame)await ReadFrameAsync(Timeout).ConfigureAwait(false);
+ if (pingAck == null || pingAck.Type != FrameType.Ping || !pingAck.AckFlag)
{
throw new Exception("Expected PING ACK");
}
+
+ Assert.Equal(pingData, pingAck.Data);
}
public async Task SendDefaultResponseHeadersAsync(int streamId)
public override bool IsHttp11 => false;
public override bool IsHttp2 => true;
}
+
+ public enum ProtocolErrors
+ {
+ NO_ERROR = 0x0,
+ PROTOCOL_ERROR = 0x1,
+ INTERNAL_ERROR = 0x2,
+ FLOW_CONTROL_ERROR = 0x3,
+ SETTINGS_TIMEOUT = 0x4,
+ STREAM_CLOSED = 0x5,
+ FRAME_SIZE_ERROR = 0x6,
+ REFUSED_STREAM = 0x7,
+ CANCEL = 0x8,
+ COMPRESSION_ERROR = 0x9,
+ CONNECT_ERROR = 0xa,
+ ENHANCE_YOUR_CALM = 0xb,
+ INADEQUATE_SECURITY = 0xc,
+ HTTP_1_1_REQUIRED = 0xd
+ }
}
// This constructor is used internally to indicate that a request was not successfully sent due to an IOException,
// and the exception occurred early enough so that the request may be retried on another connection.
- internal HttpRequestException(string message, IOException inner, bool allowRetry)
+ internal HttpRequestException(string message, Exception inner, bool allowRetry)
: this(message, inner)
{
AllowRetry = allowRetry;
if (protocolError == Http2ProtocolErrorCode.RefusedStream)
{
- http2Stream.OnRefused();
+ http2Stream.OnAbort(new Http2StreamException(protocolError), canRetry: true);
}
else
{
var errorCode = (Http2ProtocolErrorCode)BinaryPrimitives.ReadInt32BigEndian(_incomingBuffer.ActiveSpan.Slice(sizeof(int)));
if (NetEventSource.IsEnabled) Trace(frameHeader.StreamId, $"{nameof(lastValidStream)}={lastValidStream}, {nameof(errorCode)}={errorCode}");
- AbortStreams(lastValidStream, new Http2ConnectionException(errorCode));
+ StartTerminatingConnection(lastValidStream, new Http2ConnectionException(errorCode));
_incomingBuffer.Discard(frameHeader.Length);
}
{
// The connection has failed, e.g. failed IO or a connection-level frame error.
Interlocked.CompareExchange(ref _abortException, abortException, null);
- AbortStreams(0, abortException);
+ AbortStreams(abortException);
}
/// <summary>Gets whether the connection exceeded any of the connection limits.</summary>
return LifetimeExpired(nowTicks, connectionLifetime);
}
- private void AbortStreams(int lastValidStream, Exception abortException)
+ private void AbortStreams(Exception abortException)
+ {
+ // Invalidate outside of lock to avoid race with HttpPool Dispose()
+ // We should not try to grab pool lock while holding connection lock as on disposing pool,
+ // we could hold pool lock while trying to grab connection lock in Dispose().
+ _pool.InvalidateHttp2Connection(this);
+
+ lock (SyncObject)
+ {
+ if (NetEventSource.IsEnabled) Trace($"{nameof(abortException)}={abortException}");
+
+ foreach (KeyValuePair<int, Http2Stream> kvp in _httpStreams)
+ {
+ int streamId = kvp.Key;
+ Debug.Assert(streamId == kvp.Value.StreamId);
+
+ kvp.Value.OnAbort(abortException);
+ }
+
+ _httpStreams.Clear();
+
+ _disposed = true;
+ CheckForShutdown();
+ }
+ }
+
+ private void StartTerminatingConnection(int lastValidStream, Exception abortException)
{
Debug.Assert(lastValidStream >= 0);
- bool isAlreadyInvalidated = _disposed;
+
+ // Invalidate outside of lock to avoid race with HttpPool Dispose()
+ // We should not try to grab pool lock while holding connection lock as on disposing pool,
+ // we could hold pool lock while trying to grab connection lock in Dispose().
+ _pool.InvalidateHttp2Connection(this);
lock (SyncObject)
{
// We have already received GOAWAY before
// In this case the smaller valid stream is used
_lastValidStreamId = Math.Min(_lastValidStreamId, lastValidStream);
- isAlreadyInvalidated = true;
}
- if (NetEventSource.IsEnabled) Trace($"{nameof(lastValidStream)}={lastValidStream}, {nameof(abortException)}={lastValidStream}={abortException}, {nameof(_lastValidStreamId)}={_lastValidStreamId}");
+ if (NetEventSource.IsEnabled) Trace($"{nameof(lastValidStream)}={lastValidStream}, {nameof(_lastValidStreamId)}={_lastValidStreamId}");
bool hasAnyActiveStream = false;
+
foreach (KeyValuePair<int, Http2Stream> kvp in _httpStreams)
{
int streamId = kvp.Key;
Debug.Assert(streamId == kvp.Value.StreamId);
- if (streamId > lastValidStream)
+ if (streamId > _lastValidStreamId)
{
- kvp.Value.OnAbort(abortException);
-
- _httpStreams.Remove(kvp.Value.StreamId);
+ kvp.Value.OnAbort(abortException, canRetry: true);
+ _httpStreams.Remove(streamId);
}
else
{
- if (NetEventSource.IsEnabled) Trace($"Found {nameof(streamId)} {streamId} <= {lastValidStream}.");
-
+ if (NetEventSource.IsEnabled) Trace($"Found {nameof(streamId)} {streamId} <= {_lastValidStreamId}.");
hasAnyActiveStream = true;
}
}
CheckForShutdown();
}
-
- if (!isAlreadyInvalidated)
- {
- // Invalidate outside of lock to avoid race with HttpPool Dispose()
- // We should not try to grab pool lock while holding connection lock as on disposing pool,
- // we could hold pool lock while trying to grab connection lock in Dispose().
- _pool.InvalidateHttp2Connection(this);
- }
}
private void CheckForShutdown()
}
}
- public void OnAbort(Exception abortException)
+ public void OnAbort(Exception abortException, bool canRetry = false)
{
bool signalWaiter;
lock (SyncObject)
return;
}
+ // We should not retry request which have started being processed since the behavior might be unpredictable
+ // I.e. some action might have been taken based on the received data.
+ // We will bubble the exception and let user decide what to do.
+ bool isRetriable = _state == StreamState.ExpectingStatus || _state == StreamState.ExpectingHeaders;
Interlocked.CompareExchange(ref _abortException, abortException, null);
_state = StreamState.Aborted;
-
- signalWaiter = _hasWaiter;
- _hasWaiter = false;
- }
-
- if (signalWaiter)
- {
- _waitSource.SetResult(true);
- }
- }
-
- public void OnRefused()
- {
- bool signalWaiter;
- lock (SyncObject)
- {
- if (NetEventSource.IsEnabled) Trace("");
-
- if (_disposed || _state == StreamState.Aborted)
- {
- return;
- }
-
- _state = StreamState.Aborted;
- _canRetry = true;
+ _canRetry = canRetry && isRetriable;
signalWaiter = _hasWaiter;
_hasWaiter = false;
{
if (_canRetry)
{
- throw CreateRetryException();
+ throw new HttpRequestException(SR.net_http_request_aborted, _abortException, allowRetry: true);
}
throw new IOException(SR.net_http_request_aborted, _abortException);
// 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;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Test.Common;
using System.Text;
+using System.Text.Unicode;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
-using System.Collections;
namespace System.Net.Http.Functional.Tests
{
}
}
- public enum ProtocolErrors
+ private async Task<(bool, T)> IgnoreSpecificException<ExpectedException, T>(Task<T> task, string expectedExceptionContent = null) where ExpectedException : Exception
{
- NO_ERROR = 0x0,
- PROTOCOL_ERROR = 0x1,
- INTERNAL_ERROR = 0x2,
- FLOW_CONTROL_ERROR = 0x3,
- SETTINGS_TIMEOUT = 0x4,
- STREAM_CLOSED = 0x5,
- FRAME_SIZE_ERROR = 0x6,
- REFUSED_STREAM = 0x7,
- CANCEL = 0x8,
- COMPRESSION_ERROR = 0x9,
- CONNECT_ERROR = 0xa,
- ENHANCE_YOUR_CALM = 0xb,
- INADEQUATE_SECURITY = 0xc,
- HTTP_1_1_REQUIRED = 0xd
+ try
+ {
+ return (true, await task);
+ }
+ catch (ExpectedException e)
+ {
+ if (expectedExceptionContent != null)
+ {
+ if (!e.ToString().Contains(expectedExceptionContent))
+ {
+ throw;
+ }
+ }
+
+ return (false, default(T));
+ }
}
[Fact]
}
[ConditionalFact(nameof(SupportsAlpn))]
- [ActiveIssue(39013)]
public async Task GoAwayFrame_UnprocessedStreamFirstRequestFinishedFirst_RequestRestarted()
{
// This test case is similar to GoAwayFrame_UnprocessedStreamFirstRequestWaitsUntilSecondFinishes_RequestRestarted
// but is easier: we close first connection before we expect retry to happen
+ using (await Watchdog.CreateAsync())
using (var server = Http2LoopbackServer.CreateServer())
using (HttpClient client = CreateHttpClient())
{
}
[ConditionalFact(nameof(SupportsAlpn))]
- [ActiveIssue(39013)]
public async Task GoAwayFrame_UnprocessedStreamFirstRequestWaitsUntilSecondFinishes_RequestRestarted()
{
+ using (await Watchdog.CreateAsync())
using (var server = Http2LoopbackServer.CreateServer())
using (HttpClient client = CreateHttpClient())
{
[ConditionalFact(nameof(SupportsAlpn))]
public async Task GoAwayFrame_NoPendingStreams_ConnectionClosed()
{
- using (new Timer(s => Console.WriteLine(GetStateMachineData.Describe(s)), await GetStateMachineData.FetchAsync(), 60_000, 60_000))
+ using (await Watchdog.CreateAsync())
using (var server = Http2LoopbackServer.CreateServer())
using (HttpClient client = CreateHttpClient())
{
[ConditionalFact(nameof(SupportsAlpn))]
public async Task GoAwayFrame_AbortAllPendingStreams_StreamFailWithExpectedException()
{
- using (new Timer(s => Console.WriteLine(GetStateMachineData.Describe(s)), await GetStateMachineData.FetchAsync(), 60_000, 60_000))
+ using (await Watchdog.CreateAsync())
using (Http2LoopbackServer server = Http2LoopbackServer.CreateServer())
using (HttpClient client = CreateHttpClient())
{
- (_, Http2LoopbackConnection connection) = await EstablishConnectionAndProcessOneRequestAsync(client, server);
+ client.BaseAddress = server.Address;
+ server.AllowMultipleConnections = true;
- // Issue three requests
- Task<HttpResponseMessage> sendTask1 = client.GetAsync(server.Address);
- Task<HttpResponseMessage> sendTask2 = client.GetAsync(server.Address);
- Task<HttpResponseMessage> sendTask3 = client.GetAsync(server.Address);
+ (_, Http2LoopbackConnection connection) = await EstablishConnectionAndProcessOneRequestAsync(client, server);
- // Receive three requests
+ // Issue three requests, we want to make sure the specific task is related with specific stream
+ Task<HttpResponseMessage> sendTask1 = client.GetAsync("request1");
int streamId1 = await connection.ReadRequestHeaderAsync();
+
+ Task<HttpResponseMessage> sendTask2 = client.GetAsync("request2");
int streamId2 = await connection.ReadRequestHeaderAsync();
+
+ Task<HttpResponseMessage> sendTask3 = client.GetAsync("request3");
int streamId3 = await connection.ReadRequestHeaderAsync();
- Assert.InRange(streamId1, int.MinValue, streamId2 - 1);
- Assert.InRange(streamId2, int.MinValue, streamId3 - 1);
+ Assert.InRange(streamId1, 1, streamId2 - 1);
+ Assert.InRange(streamId2, streamId1 + 1, streamId3 - 1);
+ Assert.InRange(streamId3, streamId2 + 1, Int32.MaxValue);
// Send various partial responses
- // First response: Don't send anything yet
+ // First response: Don't send anything yet, request should be retried on the new connection
- // Second response: Send headers, no body yet
+ // Second response: Send headers, no body yet - request should fail
await connection.SendDefaultResponseHeadersAsync(streamId2);
- // Third response: Send headers, partial body
+ // Third response: Send headers, partial body - request should fail
await connection.SendDefaultResponseHeadersAsync(streamId3);
await connection.SendResponseDataAsync(streamId3, new byte[5], endStream: false);
- // Send a GOAWAY frame that indicates that we will abort all the requests.
- var goAwayFrame = new GoAwayFrame(0, (int)ProtocolErrors.ENHANCE_YOUR_CALM, new byte[0], 0);
- await connection.WriteFrameAsync(goAwayFrame);
+ // Ensure all sent frames are received by client
+ await connection.PingPong();
- // We will not send any more frames, so send EOF now, and ensure the client handles this properly.
- connection.ShutdownSend();
+ // Send a GOAWAY frame that indicates that we have not processed any of the requests
+ await connection.SendGoAway(0, ProtocolErrors.ENHANCE_YOUR_CALM);
+
+ Http2LoopbackConnection newConnection = await server.EstablishConnectionAsync();
+
+ HeadersFrame retriedFrame = await newConnection.ReadRequestHeaderFrameAsync();
+ int retriedStreamId = retriedFrame.StreamId;
+ Assert.InRange(retriedStreamId, 1, Int32.MaxValue);
+ string headerData = Encoding.UTF8.GetString(retriedFrame.Data.Span);
+
+ await newConnection.SendDefaultResponseHeadersAsync(retriedStreamId);
+ await newConnection.SendResponseDataAsync(retriedStreamId, new byte[3], endStream: true);
+
+ Assert.Contains("request1", headerData);
+
+ HttpResponseMessage response1 = await sendTask1;
+ Assert.Equal(HttpStatusCode.OK, response1.StatusCode);
+ await newConnection.ShutdownIgnoringErrorsAsync(retriedStreamId, ProtocolErrors.ENHANCE_YOUR_CALM);
- await AssertProtocolErrorAsync(sendTask1, ProtocolErrors.ENHANCE_YOUR_CALM);
await AssertProtocolErrorAsync(sendTask2, ProtocolErrors.ENHANCE_YOUR_CALM);
await AssertProtocolErrorAsync(sendTask3, ProtocolErrors.ENHANCE_YOUR_CALM);
- // Now that all pending responses have been sent, the client should close the connection.
await connection.WaitForConnectionShutdownAsync();
-
- // New request should cause a new connection
- await EstablishConnectionAndProcessOneRequestAsync(client, server);
}
}
<Compile Include="TestHelper.cs" />
<Compile Include="DefaultCredentialsTest.cs" />
<Compile Include="ThrowingContent.cs" />
+ <Compile Include="Watchdog.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetGroup)' == 'netcoreapp'">
<Compile Include="CustomContent.netcore.cs" />
--- /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;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace System.Net.Http.Functional.Tests
+{
+ /// <summary>
+ /// This is using similar trick to GetStateMachineData
+ /// If marked test runs for more than 60s it will print machine state and make sure it fails
+ /// Usage (await MUST be run directly in the test, should not be called from other async method):
+ /// using (await Watchdog.CreateAsync())
+ /// {
+ /// // test code
+ /// }
+ /// </summary>
+ internal class Watchdog : ICriticalNotifyCompletion
+ {
+ private object _box;
+
+ private Watchdog() { }
+
+ public static Watchdog CreateAsync()
+ => new Watchdog();
+
+ public IDisposable GetResult()
+ => new WatchdogImpl(_box);
+
+ public Watchdog GetAwaiter() => this;
+ public bool IsCompleted => false;
+ public void OnCompleted(Action continuation) => UnsafeOnCompleted(continuation);
+ public void UnsafeOnCompleted(Action continuation)
+ {
+ _box = continuation.Target;
+ Task.Run(continuation);
+ }
+
+ private class WatchdogImpl : IDisposable
+ {
+ private bool _passed = true;
+ private Timer _timer;
+
+ public WatchdogImpl(object stateMachineData)
+ {
+ _timer = new Timer(s =>
+ {
+ _passed = false;
+ Console.WriteLine(GetStateMachineData.Describe(s));
+ },
+ stateMachineData,
+ 60_000,
+ 60_000);
+ }
+
+ public void Dispose()
+ {
+ _timer.Dispose();
+ Assert.True(_passed);
+ }
+ }
+ }
+}