0

I'm using HttpClient to upload files to an API that requires the Content-MD5 header. How can I get the full request body from HttpClient before sending it so I can run MD5 on the content and use that in the request header? Note that I also need to include the headers in between the multipart form data, i.e. Content-Disposition and all other headers that are in each part.

I'm using the code below, taken from this answer.

private System.IO.Stream Upload(string url, string param1, Stream fileStream, byte [] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);
    using (var client = new HttpClient())
    using (var formData = new MultipartFormDataContent())
    {
        formData.Add(stringContent, "param1", "param1");
        formData.Add(fileStreamContent, "file1", "file1");
        formData.Add(bytesContent, "file2", "file2");
        var response = client.PostAsync(url, formData).Result;
        if (!response.IsSuccessStatusCode)
        {
            return null;
        }
        return response.Content.ReadAsStreamAsync().Result;
    }
}
Community
  • 1
  • 1
Omar
  • 39,496
  • 45
  • 145
  • 213
  • PLEASE never use : using(var c = new HttpClient()) --> https://stackoverflow.com/a/54707165/1498669 – Bernhard Jun 28 '21 at 11:15

3 Answers3

2

Now, I admit ahead of time that I have not tested this code because I do not have a test site setup to hit. However, I have tested up to the point of Posting the data in LINQPad and the code doesn't error and the MD5 hash is set. The following should work for what you want to do:

private System.IO.Stream Upload(string url, string param1, Stream fileStream, byte[] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);

    using (HttpClient client = new HttpClient())
    {
        using (MultipartFormDataContent formData = new MultipartFormDataContent())
        {
            formData.Add(stringContent, "param1", "param1");
            formData.Add(fileStreamContent, "file1", "file1");
            formData.Add(bytesContent, "file2", "file2");

            using (MD5 md5Hash = MD5.Create())
            {
                formData.Headers.ContentMD5 = md5Hash.ComputeHash(formData.ReadAsByteArrayAsync().Result);
            }

            var response = client.PostAsync(url, formData).Result;
            if (!response.IsSuccessStatusCode)
            {
                return null;
            }

            return response.Content.ReadAsStreamAsync().Result;
        }
    }
}
Adam Gritt
  • 2,654
  • 18
  • 20
2

How big are your files? The problem with Adam's answer is that it buffers the entire contents of the file in memory. This may cause your program to run out of memory for large files, or perform poorly because of excessive disk swapping.

In fact, I've found that even MultipartFormDataContent.ReadAsStreamAsync() will buffer the entire contents into memory (probably by calling MultipartFormDataContent.LoadIntoBufferAsync()). The problem doesn't seem to exist when using StreamContent.ReadAsStreamAsync(), so it seems like the only solution if you're running into memory issues is to write your own implementation of MultipartFormDataContent that does not buffer the entire contents but makes use of StreamContent.ReadAsStreamAsync().

Note, I found that MultipartFormDataContent.CopyToAsync() will not buffer the entire contents in memory provided the receiving stream does not. It may be worth a shot to write a Stream implementation that functions as a sort of pipe, where any bytes written to it are immediately consumed by md5Hash.ComputeHash(Stream).

EDIT: these are my experiences on .NET 4.0. I heard .NET 4.5 has behaves differently as far as buffering on the client goes, so I'm not sure how MultipartFormDataContent.ReadAsStreamAsync() performs on it.

Kevin Jin
  • 1,536
  • 4
  • 18
  • 20
2

The generic way to handle this sort of with HttpClient is to use a HttpMessageHandler as you might other work to do e.g. adding authorization headers, signing the message etc.

I've also re-written this using Task based syntax as it's more ideomatic HttpClient - the caller can call .Result as needed.

private await Task<System.IO.Stream> Upload(string url, string param1, Stream fileStream, byte[] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);

    var handler = new HttpClientHandler();
    var md5Handler = new RequestContentMd5Handler();
    md5Handler.InnerHandler = handler;

    using (HttpClient client = new HttpClient(md5Handler))
    {
        using (MultipartFormDataContent formData = new MultipartFormDataContent())
        {
            formData.Add(stringContent, "param1", "param1");
            formData.Add(fileStreamContent, "file1", "file1");
            formData.Add(bytesContent, "file2", "file2");

            using (var response = await client.PostAsync(url, formData))
            {
                if (!response.IsSuccessStatusCode)
                {
                    return null;
                }

                return await response.Content.ReadAsStreamAsync();
            }
        }
    }
}

Also, it's generally poor practice to re-create the HttpClient on each request (see What is the overhead of creating a new HttpClient per call in a WebAPI client? etc), but I've left that here in keeping with the style of the question.

Here's the handler used...

/// <summary>
/// Handler to assign the MD5 hash value if content is present
/// </summary>
public class RequestContentMd5Handler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Content == null)
        {
            return await base.SendAsync(request, cancellationToken);
        }

        await request.Content.AssignMd5Hash();

        var response = await base.SendAsync(request, cancellationToken);

        return response;
    }
}

And the extensions methods...

    /// <summary>
    /// Compute and assign the MD5 hash of the content.
    /// </summary>
    /// <param name="httpContent"></param>
    /// <returns></returns>
    public static async Task AssignMd5Hash(this HttpContent httpContent)
    {
        var hash = await httpContent.ComputeMd5Hash();

        httpContent.Headers.ContentMD5 = hash;
    }

    /// <summary>
    /// Compute the MD5 hash of the content.
    /// </summary>
    /// <param name="httpContent"></param>
    /// <returns></returns>
    public static async Task<byte[]> ComputeMd5Hash(this HttpContent httpContent)
    {
        using (var md5 = MD5.Create())
        {
            var content = await httpContent.ReadAsStreamAsync();
            var hash = md5.ComputeHash(content);
            return hash;
        }
    }

Makes it easy to unit test the various parts.

Paul Hatcher
  • 7,342
  • 1
  • 54
  • 51