Detect client disconnect with HttpListener

Short answer: you can't. If a client stops talking, the underlying socket may stay open and won't ever close; it'll just timeout. The way to detect this is to attempt to perform an action on that connection and if the connection is no longer valid, it'll throw some sort of exception depending on what happened. If you use HttpListener asynchronously, it may clean up your code a bit in terms of a try/catch but unfortunately that's what you're stuck with. There is no event that will fire if the client disconnects.


You can! Two options I've found are with reflection or unsafe code. Both options give you a client disconnect token from a method that looks like this:

CancellationToken GetClientDisconnectToken(HttpListenerRequest request)

To implement this via refection, I found HttpListener actually implements client disconnect notifications for the built-in authentication implementation. I created a type who derives from Hashtable, the structure HttpListener uses for outstanding client disconnect notifications, to tap in to that code outside of its intended purpose.

For every request there is a ConnectionId used by HTTP.SYS. This code uses reflection and creates a Func<> to obtain this id for any HttpListenerRequest:

private static Func<HttpListenerRequest, ulong> GetConnectionId()
{
    var field = typeof(HttpListenerRequest).GetField("m_ConnectionId",
      BindingFlags.Instance | BindingFlags.NonPublic);

    if (null == field)
        throw new InvalidOperationException();

    return request => (ulong)field.GetValue(request);
}

The next bit of code is a little more complicated:

private static Func<ulong, IAsyncResult> GetRegisterForDisconnectNotification(HttpListener httpListener)
{
    var registerForDisconnectNotification = typeof(HttpListener)
      .GetMethod("RegisterForDisconnectNotification", BindingFlags.Instance | BindingFlags.NonPublic);

    if (null == registerForDisconnectNotification)
        throw new InvalidOperationException();

    var finishOwningDisconnectHandling =
      typeof(HttpListener).GetNestedType("DisconnectAsyncResult", BindingFlags.NonPublic)
        .GetMethod("FinishOwningDisconnectHandling", BindingFlags.Instance | BindingFlags.NonPublic);

    if (null == finishOwningDisconnectHandling)
        throw new InvalidOperationException();

    IAsyncResult RegisterForDisconnectNotification(ulong connectionId)
    {
        var invokeAttr = new object[] { connectionId, null };
        registerForDisconnectNotification.Invoke(httpListener, invokeAttr);

        var disconnectedAsyncResult = invokeAttr[1];
        if (null != disconnectedAsyncResult) 
            finishOwningDisconnectHandling.Invoke(disconnectedAsyncResult, null);

        return disconnectedAsyncResult as IAsyncResult;
    }

    return RegisterForDisconnectNotification;
}

This reflection creates a Func<> whose input is a ConnectionId and returns an IAsyncResult containing the state of the inflight request. Internally, this calls a private method on HttpListener:

private unsafe void RegisterForDisconnectNotification(ulong connectionId,
      ref HttpListener.DisconnectAsyncResult disconnectResult)

As the name implies, this method calls a Win32 API to be notified of a client disconnect. Immediately after, using the result of that method I call a private method on a nested type: void HttpListener.DisconnectedAsyncResult.FinishOwningDisconnectHandling(), if the connection is still open. This method changes the state of this structure from "in HandleAuthentication" to "Normal", which is the state it needs to be in to invoke the IO completion callback who will call Remove on the HashTable. Intercepting this call turns out to be pretty simple - create a derived type and override Remove:

public override void Remove(object key)
{
    base.Remove(key);

    var connectionId = (ulong)key;
    if (!_clientDisconnectTokens.TryRemove(connectionId, out var cancellationTokenSource))
        return;

    Cancel(cancellationTokenSource);
}

private static void Cancel(CancellationTokenSource cancellationTokenSource)
{
    // Use TaskScheduler.UnobservedTaskException for caller to catch exceptions
    Task.Run(() => cancellationTokenSource.Cancel());
}

Calling Cancel tends to throw, so we invoke this using TPL so you can catch any exception thrown during cancel by subscribing to TaskScheduler.UnobservedTaskException.

What left?

  • creation of the HttpListenerHashtable derived type
  • storage of in-flight CancellationTokenSource instances
  • set or replace the HashTable field of HttpListener with the HttpListenerHashtable (it's best to do this right after creating the HttpListener instance)
  • handle the request of a disconnect token, and the client disconnects while the code is executing

All of which are addressed in the full source.