Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 64 additions & 10 deletions test/Shared/TestHttpServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ private sealed class RunningServer : IDisposable
private readonly Task httpListenerTask;
private readonly HttpListener listener;
private readonly TaskCompletionSource<bool> initialized = new(TaskCreationOptions.RunContinuationsAsynchronously);
private readonly CancellationTokenSource cancellationTokenSource = new();

public RunningServer(Action<HttpListenerContext> action, string host, int port)
{
Expand All @@ -66,18 +67,21 @@ public RunningServer(Action<HttpListenerContext> 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();
}
Expand All @@ -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<HttpListenerContext> 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)
{
Expand All @@ -114,6 +147,27 @@ private async Task ListenAsync(Action<HttpListenerContext> 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.
}
}
}
Expand Down
Loading