From c292d1fe73f06d59e659b9bd199c25d7f4febfe6 Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 20 Apr 2026 10:37:08 +0100 Subject: [PATCH 1/3] [Shared] Fix flaky tests and avoid deadlock - Attempt to fix flaky test shutdown. - Avoid deadlock when used with `HttpClient.Send()`. --- test/Shared/TestHttpServer.cs | 48 ++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/test/Shared/TestHttpServer.cs b/test/Shared/TestHttpServer.cs index 12f43c6791..22c56754fa 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); } - 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,20 +91,25 @@ public void Dispose() } } - private bool IsListenerShutdownException(Exception ex) => + private static bool IsResponseAlreadyClosedException(Exception ex) => ex is ObjectDisposedException || - (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 6 or 995 or 10057)) || + (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 1 or 6 or 995 or 10057)); + + private bool IsListenerShutdownException(Exception ex) => + 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); + context = await this.listener.GetContextAsync().ConfigureAwait(false); action(context); } catch (Exception ex) @@ -114,6 +123,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. } } } From 67fbde24e6ca502d9048f673f3ad5d584f0c25aa Mon Sep 17 00:00:00 2001 From: Martin Costello Date: Mon, 20 Apr 2026 10:57:28 +0100 Subject: [PATCH 2/3] [Shared] Unwrap task Apply code review suggestion. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/Shared/TestHttpServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Shared/TestHttpServer.cs b/test/Shared/TestHttpServer.cs index 22c56754fa..dcc20bb2e3 100644 --- a/test/Shared/TestHttpServer.cs +++ b/test/Shared/TestHttpServer.cs @@ -71,7 +71,7 @@ public RunningServer(Action action, string host, int port) () => this.ListenAsync(action), this.cancellationTokenSource.Token, TaskCreationOptions.LongRunning, - TaskScheduler.Default); + TaskScheduler.Default).Unwrap(); } public void Start() => this.initialized.Task.GetAwaiter().GetResult(); From 41362243bf05e775199e419b3b36250ec2bd8963 Mon Sep 17 00:00:00 2001 From: martincostello Date: Mon, 20 Apr 2026 11:19:29 +0100 Subject: [PATCH 3/3] [Shared] Fix flakiness Apply Copilot suggestions to fix. --- test/Shared/TestHttpServer.cs | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/test/Shared/TestHttpServer.cs b/test/Shared/TestHttpServer.cs index dcc20bb2e3..74f9ae9c4d 100644 --- a/test/Shared/TestHttpServer.cs +++ b/test/Shared/TestHttpServer.cs @@ -91,9 +91,23 @@ public void Dispose() } } - private static bool IsResponseAlreadyClosedException(Exception ex) => - ex is ObjectDisposedException || - (ex is HttpListenerException httpEx && (httpEx.ErrorCode is 1 or 6 or 995 or 10057)); + 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) => IsResponseAlreadyClosedException(ex) || @@ -110,7 +124,17 @@ private async Task ListenAsync(Action action) try { context = await this.listener.GetContextAsync().ConfigureAwait(false); - action(context); + + 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) { @@ -143,7 +167,7 @@ private void TryCloseResponse(HttpListenerResponse response) } catch (Exception ex) when (IsResponseAlreadyClosedException(ex)) { - // The handler completed the response explicitly. + // The handler completed the response explicitly or the client disconnected. } } }