C# – Disposing the request HttpContent when using HttpClient

In versions before .NET Core 3.0 (including .NET Framework), HttpClient disposes the request HttpContent object for you. This is surprising default behavior (a violation of the principle of least surprise for sure). This causes multiple problems, but one of the main problems is it prevents you from reusing the HttpContent object (you’re greeted with an ObjectDisposedException if you try).

There are many reasons why you might want to reuse an HttpContent object. Perhaps you’re implementing retry logic and don’t want to recreate the content for every attempt. Or perhaps you want to cache the HttpContent for future requests.

The .NET team recognized this default behavior as a design flaw and fixed it in .NET Core 3.0. This means that you’re now responsible for disposing the request HttpContent object. You can dispose it when it makes sense in your specific situation.

I’ll show a real world example of doing request retries before and after this “automatic disposal” behavior was changed.

Example of retrying requests – before and after the fix

In this example, I’ll send a file in a request with retry attempts. I’ll implement retries with Polly.

.NET Core 3.0 (including .NET Framework)

In this version with the surprising “automatic disposal” behavior, HttpClient disposes the HttpContent object. So on retry attempts, a new HttpContent has to be created.

using Polly;

var retryPolicy = Policy.Handle<HttpRequestException>()
	.WaitAndRetryAsync(retryCount: 3, sleepDurationProvider: _ => TimeSpan.FromSeconds(5));

return await retryPolicy.ExecuteAsync(async () =>
{
	var multipartFormContent = new MultipartFormDataContent();
	var fileStreamContent = new StreamContent(File.OpenRead(@"C:\bigfile.zip"));
	fileStreamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
	multipartFormContent.Add(fileStreamContent, name: "file", fileName: "bigfile.zip");

	Console.WriteLine("Sending request");
	var response = await httpClient.PostAsync("http://localhost:12345/files/", multipartFormContent);
	response.EnsureSuccessStatusCode();
	return await response.Content.ReadAsStringAsync();
});
Code language: C# (cs)

Note: If you’re not familiar with doing retries with Polly, this is equivalent to doing requests in a loop.

The MultipartFormDataContent object has to be created for each request attempt, along with all of the associated objects. This is because when HttpClient disposes the MultipartFormDataContent, it cascades the disposal. Therefore, you can’t reuse the StreamContent or FileStream.

.NET Core 3.0 and above

After the “automatic disposal” design flaw was fixed in .NET Core 3.0, you can reuse the request HttpContent object and then dispose it when appropriate for your scenario.

In this example, the MultipartFormDataContent object (and associated file stream) is created once and reused in every request attempt. It’s then disposed at the end (via a using block):

using Polly;

var retryPolicy = Policy.Handle<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.TooManyRequests)
	.WaitAndRetryAsync(retryCount: 3,sleepDurationProvider: _ => TimeSpan.FromSeconds(5));

using (var multipartFormContent = new MultipartFormDataContent())
{
	var fileStreamContent = new StreamContent(File.OpenRead(@"C:\bigfile.zip"));
	fileStreamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
	multipartFormContent.Add(fileStreamContent, name: "file", fileName: "bigfile.zip");

	return await retryPolicy.ExecuteAsync(async () =>
	{
		Console.WriteLine("Send request");
		var response = await httpClient.PostAsync("http://localhost:12345/files/", multipartFormContent);
		response.EnsureSuccessStatusCode();
		return await response.Content.ReadAsStringAsync();
	});
}
Code language: C# (cs)

Because you can dispose the HttpContent yourself, this means you can separate the “content creation” logic from the “send request” logic, whereas you couldn’t separate that logic before. This allows you to cache the HttpContent object for reuse.