Upgrading Mono's HttpWebRequest

Miguel de Icaza releases

After almost 12 years, we are upgrading the guts of HttpWebRequest, the engine that powers the basic HTTP client stack in Mono.

.NET offers two sets of HTTP client APIs, the original HttpWebRequest that offers a comprehensive and configurable API that can communicate with HTTP 1.x servers using the original .NET 1.0 programming model, and a modern, pluggable and more limited API in the form of HttpClient which can also talk to HTTP 2.0 servers.

HttpClient is an interesting API as it has been designed to allow for different providers to be used and was also designed with async programming support in mind.

On each platform, HttpClient tries to use the best available transport:

Host/Runtime Backend
Windows/.NET Framework HttpWebRequest
Windows/Mono HttpWebRequest
Windows/UWP Windows native WinHttpHandler (HTTP 2.0 capable)
Windows/.NET Core Windows native WinHttpHandler (HTTP 2.0 capable)
Android/Xamarin Default to Android’s HTTP transport
Can be configured to be HttpWebRequest.
iOS, tvOS, watchOS/Xamarin Default to NSUrlSession (HTTP 2.0 capable)
Can be configured to use HttpWebRequest.
macOS/Xamarin Default to NSUrlSession (HTTP 2.0 capable)
Can be configured to use HttpWebRequest
macOS/Mono HttpWebRequest
macOS/.NET Core libcurl-based HTTP transport (HTTP 2.0 capable)
Linux/Mono HttpWebRequest
Linux/.NET Core libcurl-based HTTP transport (HTTP 2.0 capable)

As you can see, while HttpWebRequest is not the default across the board and lacks HTTP 2.0 capabilities, it is still available in various configurations in Mono-powered stacks (Mono and Xamarin).

Mono’s original HttpWebRequest stack was written in 2004 and was built using the asynchronous APIs that were available in .NET 1.0, that is the BeginInvoke / EndInvoke patterns, a pattern that was heavily based on queuing work, waiting for a result and resuming execution at a later point.

While the code grew in capabilities, features and reliability over the years, all of this was built on top of these 2004-era programming idioms. The code is difficult to read, to follow and understand. Simple bugs can take a long time to fix, and subtle problems can burn the most passionate developer. It has been a source of frustration for both us, and our users when a rare bug comes up in this stack.

At one point we tried to use the .NET Reference Source implementation, but that version was just too difficult to extract as it relies on many internals that do not exist in Mono, and in turn relies on Windows capabilities that we would have needed to support [1]. But this code also used those old idioms and while it might have fewer bugs and we might increase the compatibility of the stacks, the extraction was too complicated.

Over the years we have embraced expedience in the form of band-aids for this code, over the right fix, given just how complex the code had gotten. We are facing a couple of corner cases that are hard to fix and we wanted to fix the code for good, not apply another band aid.

So we decided to refactor the code from the old idioms from 2004 into 2017 idioms, using everything that is available to us in C# 7.

Chief among those capabilities is the use of async programming that removes both the complexity from the code and makes the code a pleasure to read and understand. It has taken us a few weeks to do the port, and we are currently validating every known scenario and test suite against it, but things are looking very promising.

You can track the state of this effort on Martin’s GitHub pull request.

[1] At one point we did write such a layer, the effort still lives dormant on a branch, for anyone that might be interested in resuming that work.