Beware of These Hidden Pitfalls When Implementing Custom HttpContent in C#

by Arkadiusz Paliński


Implementing custom HttpContent in C# can unlock powerful flexibility, but it comes with subtle details that can lead to tricky bugs. These nuances aren’t fully covered in the official documentation, yet they’re crucial for ensuring reliable HTTP requests. In this article, we’ll explore hidden aspects of custom HttpContent implementations based on our experiences with RavenDB.

The importance of flushing in SerializeToStreamAsync to force sending the headers

The first issue we encountered involved occasional request hangs during our test suite execution with the RavenDB server. After exploring related issues on GitHub, we found relevant discussions:

Since SerializeToStreamAsync is asynchronous, content may not be immediately available when the request is sent. To force the headers to go out before the body, we discovered we needed to explicitly call FlushAsync on the request body stream. As pointed out in Issue #30295 (comment):

“If you aren’t going to immediately send request body data, but you do want the request to be sent to the server immediately, then you need to call FlushAsync on the request body to guarantee this.”

Here’s a code example:

protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
     // Immediately flush request stream to send headers
     await stream.FlushAsync().ConfigureAwait(false); 
  
    // Actual write of data to request stream
    ...       
}



Without explicitly flushing, the headers are not guaranteed to be sent immediately. Libraries like gRPC for .NET also use FlushAsync in their PushStreamContent, which inherits from HttpContent, to ensure headers are sent promptly. This behavior is confirmed by tests in the System.Net.Http package (Issue #96223 (comment)).

Adding this explicit FlushAsync call can resolve delay or deadlock issues, as seen in our Pull Request #18552.

Understanding HttpClient.SendAsync completion timing

The next issue was even more subtle and surprising. According to Issue #107082 (comment):


Yes, there are cases where the task returned by HttpClient.SendAsync may be completed before the call to the request content’s SerializeToStreamAsync completes.”


We initially assumed that once SendAsync completed, it was safe to release resources allocated for the request body. However, we found that SerializeToStreamAsync could still be running, leading to premature memory release and occasional AccessViolationException errors.

To fix this, we added a check in a finally block to wait for SerializeToStreamAsync to finish if it’s still running:

try
{
  	return await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
}
finally
{
    if (request.Content is BlittableJsonContent bjc)
    {
        var requestBodyTask = bjc.EnsureCompletedAsync();

      	if (requestBodyTask.IsCompleted == false)
            await requestBodyTask.ConfigureAwait(false);
    }
}

The EnsureCompletedAsync method returns a task that completes only when SerializeToStreamAsync is done. This way, we avoid freeing memory prematurely, as implemented in our Pull Request #19168.

Additional Things to Watch For

In addition to the two main issues, several other considerations were highlighted in Issue #107082 (comment):

  • SerializeToStreamAsync not being called. Be prepared for situations where it might never be called, especially if the request is canceled early.
  • Multiple calls to SerializeToStreamAsync on retries. If a request is retried, SerializeToStreamAsync could be called multiple times. Make sure your implementation can handle this, or that it properly throws if it cannot support retries.

In our BlittableJsonContent implementation, we handle these scenarios. If SerializeToStreamAsync isn’t called, EnsureCompletedAsync simply returns a completed task to avoid indefinite waiting. For retries, our implementation throws an exception if SerializeToStreamAsync has already consumed some of the data.

Conclusion

Implementing custom HttpContent can offer significant flexibility, but careful attention to details like FlushAsync and HttpClient.SendAsync completion timing is essential. Awareness of these nuances helps you avoid subtle bugs and create reliable HTTP requests. Learning from our experiences in RavenDB can ensure that your code handles these critical details effectively.

Woah, already finished? 🤯

If you found the article interesting, don’t miss a chance to try our database solution – totally for free!

Try now try now arrow icon