Skip to content

NullReferenceException when closing HttpListener #63345

@kevingosse

Description

@kevingosse

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:

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:

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions