-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Description
Description
We ran into an issue when trying to abort an HttpListener.
Our code looks like:
try
{
var ctx = _listener.GetContext();
// Do stuff
}
catch (HttpListenerException)
{
// listener was stopped,
}
catch (ObjectDisposedException)
{
// the response has been already disposed.
}
catch (Exception) when (!_listener.IsListening)
{
// we don't care about any exception when listener is stopped
}At some point, another thread calls Abort on the HttpListener. This randomly causes _listener.GetContext() to throw a NullReferenceException:
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object.
at System.Net.HttpListener.GetContext()
at Datadog.Trace.TestHelpers.MockTelemetryAgent`1.HandleHttpRequests()
It seems it happens when trying to read session.RequestQueueHandle because currentSession is null:
runtime/src/libraries/System.Net.HttpListener/src/System/Net/Windows/HttpListener.Windows.cs
Lines 511 to 521 in dfd618d
| HttpListenerSession session = _currentSession!; | |
| while (true) | |
| { | |
| while (true) | |
| { | |
| if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Calling Interop.HttpApi.HttpReceiveHttpRequest RequestId: {requestId}"); | |
| uint bytesTransferred = 0; | |
| statusCode = | |
| Interop.HttpApi.HttpReceiveHttpRequest( | |
| session.RequestQueueHandle, |
(it's set to null as a side-effect of calling
Abort)
It didn't happen in .NET Core 3.1 and before, because there was no session object and the code would directly access the handle: https://github.com/dotnet/corefx/blob/release/3.1/src/System.Net.HttpListener/src/System/Net/Windows/HttpListener.Windows.cs#L588
Interop.HttpApi.HttpReceiveHttpRequest(
session.RequestQueueHandle,The error code would then be handled and transformed into a HttpListenerException
Also, note that my code fails to catch the exception despite the catch (Exception) when (!_listener.IsListening) that was added in desperation. That's because the Abort method first sets the session to null (as part of CloseRequestQueueHandle and only updates the state at the very end:
runtime/src/libraries/System.Net.HttpListener/src/System/Net/Windows/HttpListener.Windows.cs
Lines 397 to 427 in dfd618d
| public void Abort() | |
| { | |
| lock (_internalLock) | |
| { | |
| try | |
| { | |
| if (_state == State.Closed) | |
| { | |
| return; | |
| } | |
| // Just detach and free resources. Don't call Stop (which may throw). Behave like v1: just | |
| // clean up resources. | |
| if (_state == State.Started) | |
| { | |
| DetachRequestQueueFromUrlGroup(); | |
| CloseRequestQueueHandle(); | |
| } | |
| CleanupV2Config(); | |
| } | |
| catch (Exception exception) | |
| { | |
| if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"Abort {exception}"); | |
| throw; | |
| } | |
| finally | |
| { | |
| _state = State.Closed; | |
| } | |
| } | |
| } |
Since the method is going to update the state no matter what, I think it should try to do so as soon as possible.
Reproduction Steps
It's a bit hard to reproduce because it's a tight race condition, but basically HttpListener.GetContext() has to be called at the same time as HttpListener.Abort()
Expected behavior
HttpListener.GetContext() should throw either HttpListenerException or ObjectDisposedException.
Actual behavior
HttpListener.GetContext() throws NullReferenceException
Regression?
This worked fine in 3.1.