I often have “The Moment”. The moment where you become hyper-aware of how much nonsense you’re putting up with when using a tool or codebase or something and just how much you have to keep in mind when using it. It doesn’t have to be the first time, you can happily drift in and out of awareness in my experience, but until things are better The Moment is always looming. HttpClient once again gave me The Moment this week when I was helping someone understand a bug they were dealing with.

I don’t think I’m really writing anything profound or new here, just screaming into a void full of screams, but with my own voice.

Here’s three gotchas that came to the forefront of my brain this week, I’m sure I’ve got more in there but will wait for a future code review to eke them out.

1. HttpClient implements IDisposable

This one has been documented to death now, everyone’s already had to google this a hundred times to remind themselves if it’s correct to always dispose of it after use, or create a singleton and share that everywhere (the correct answer, hilariously, is to do neither of those). The internals of the client do some magic things with socket use that mean you shouldn’t chuck it away all the time, and similarly holding one for too long means it isn’t resilient to DNS changes.

This is thankfully one of the more “old news” problems that a lot of people have become savvy to, but still contributes to it being a bit of a minefield.

Microsoft have a great post on how to go less wrong with all this.

2. DefaultRequestHeaders misuse

This one is less of a gotcha if you really think about object lifetimes and parallel execution but has still come up as a misunderstanding more than I’d have liked it to. It coincides with the first point - HttpClient should not be created and thrown away every isolated use, so its properties have a longer-than-transient lifetime too.

You can set headers per-request but the efficient among you might prefer to set a default request header on the client itself. This makes absolute sense for some things (think Accept: application/json for a client only ever used to access a json api, or a custom User-Agent string) but can be a dangerous way to bleed the wrong thing across requests. If you set an authorization header as a default header, for example, any usage of the http client in parallel with the thread that sets that header will snag that authorization token for free. Something like this:

httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
var ochreResource = await httpClient.GetAsync("ochre");
var lilacResource = await httpClient.GetAsync("lilac");

becomes quite deadly in a context where different tokens may be used. Invocation 1 might add token A, request ochre, then invocation 2 might override that with token B, invocation 1 then requests lilac using token B. Or any other possible reordering.

3. Inheriting the ‘quirks’ of Uri for free

The HttpClient uses the Uri class for… well, uris. Is that a problem? Not necessarily, but if Uri has any quirks then HttpClient gets those for free. What’s wrong in the following example?

var httpClient = new HttpClient
    BaseAddress = new Uri("https://api.example.com/v1")
var ochre = await httpClient.GetAsync("/ochre");

This will call GET https://api.example.com/ochre. What. Where’d the version go? How do I fix that?

var httpClient = new HttpClient
    BaseAddress = new Uri("https://api.example.com/v1/") // note the trailing slash
var ochre = await httpClient.GetAsync("/ochre");

This is a quirk of Uri combination which I think a lot of people do not expect using the uri constructor. You can replicate the quirk directly:

var baseAddress = new Uri("https://api.example.com/v1");
var fullAddress = new Uri(baseAddress, "/ochre");
var unexpectedResult = fullAddress.ToString(); // https://api.example.com/ochre

Final words

I don’t believe or suggest that HttpClient was designed without thought or care, it’s doing a lot of things good and right. But HttpClient today is a bit of a minefield of quirks that make it easy to get some stuff wrong, and it’s not exactly a niche-use-case class. Keep an eye on these and do some solid reading on the thing if you’re a bit unfamiliar with it. It’s not as intuitive as you’d hope.