diff --git a/test/Shared/TestHttpServer.cs b/test/Shared/TestHttpServer.cs index 12f43c6791..74f9ae9c4d 100644 --- a/test/Shared/TestHttpServer.cs +++ b/test/Shared/TestHttpServer.cs @@ -58,6 +58,7 @@ private sealed class RunningServer : IDisposable private readonly Task httpListenerTask; private readonly HttpListener listener; private readonly TaskCompletionSource initialized = new(TaskCreationOptions.RunContinuationsAsynchronously); + private readonly CancellationTokenSource cancellationTokenSource = new(); public RunningServer(Action action, string host, int port) { @@ -66,18 +67,21 @@ public RunningServer(Action action, string host, int port) this.listener.Prefixes.Add($"http://{host}:{port}/"); this.listener.Start(); - this.httpListenerTask = Task.Run(() => this.ListenAsync(action)); + this.httpListenerTask = Task.Factory.StartNew( + () => this.ListenAsync(action), + this.cancellationTokenSource.Token, + TaskCreationOptions.LongRunning, + TaskScheduler.Default).Unwrap(); } - public void Start() - { - this.initialized.Task.GetAwaiter().GetResult(); - } + public void Start() => this.initialized.Task.GetAwaiter().GetResult(); public void Dispose() { try { + this.cancellationTokenSource.Cancel(); + this.cancellationTokenSource.Dispose(); this.listener.Close(); this.httpListenerTask.GetAwaiter().GetResult(); } @@ -87,21 +91,50 @@ public void Dispose() } } + private static bool IsResponseAlreadyClosedException(Exception exception) + { + for (var ex = exception; ex is not null; ex = ex.InnerException) + { + if (ex is ObjectDisposedException) + { + return true; + } + + if (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 1 or 6 or 995 or 10057)) + { + return true; + } + } + + return false; + } + private bool IsListenerShutdownException(Exception ex) => - ex is ObjectDisposedException || - (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 6 or 995 or 10057)) || + IsResponseAlreadyClosedException(ex) || (ex is InvalidOperationException && !this.listener.IsListening); private async Task ListenAsync(Action action) { this.initialized.TrySetResult(true); - while (true) + while (!this.cancellationTokenSource.IsCancellationRequested) { + HttpListenerContext? context = null; + try { - var context = await this.listener.GetContextAsync().ConfigureAwait(false); - action(context); + context = await this.listener.GetContextAsync().ConfigureAwait(false); + + try + { + action(context); + } + catch (Exception ex) when (IsResponseAlreadyClosedException(ex)) + { + // Client disconnected / response stream already torn down while the handler + // was writing the response or disposing response writer/stream. + // Treat as non-fatal and continue accepting new requests. + } } catch (Exception ex) { @@ -114,6 +147,27 @@ private async Task ListenAsync(Action action) throw; } + finally + { + if (context is not null) + { + this.TryCloseResponse(context.Response); + } + } + + await Task.Yield(); + } + } + + private void TryCloseResponse(HttpListenerResponse response) + { + try + { + response.Close(); + } + catch (Exception ex) when (IsResponseAlreadyClosedException(ex)) + { + // The handler completed the response explicitly or the client disconnected. } } }