Roland Deschain
Roland Deschain

Reputation: 2850

How to prevent HttpClient.PostAsync to fill up memory?

I have the following function in my blazor webassembly app to upload large files to the REST server:

private async Task<FileModel?> UploadSingleFile(IBrowserFile file)
{
    using var content = new MultipartFormDataContent();
    using Stream fileStream = file.OpenReadStream(file.Size);
    var fileContent = new StreamContent(fileStream);
    fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);
    fileContent.Headers.ContentLength = fileStream.Length;
    content.Add(fileContent, file.Name);
    var response = await HttpClient.PostAsync($"File/{_selectedDataset.ID}", content);

    if (response.IsSuccessStatusCode)
    {
        var addedFile = (await response.Content.ReadFromJsonAsync<IEnumerable<FileModel>>())?.FirstOrDefault();
        return addedFile;
    } // else do something

    return null;
}

The function above should be able to stream large files to the API server. It works fine with moderately small files, however testing with bigger files resulted in a out-of-memory exception on the client sied. Even before hitting the server enpoint, the HttpClient.PostAsync() method fills the clients memory, even though the stream doesn't seem to be copied somewhere by me.

I have already tried to use SendAsync() with an HttpRequestMessage with an TransferEncodingChunked = true header added, but got the same result. Somewhere behind the scenes the whole data stream is copied to memory. How can I avoid this?

For completness, here is the full exception

System.OutOfMemoryException: Out of memory at System.IO.MemoryStream.set_Capacity(Int32 value) at System.IO.MemoryStream.EnsureCapacity(Int32 value) at System.IO.MemoryStream.Write(Byte[] buffer, Int32 offset, Int32 count) at System.Net.Http.HttpContent.LimitMemoryStream.Write(Byte[] buffer, Int32 offset, Int32 count) at System.IO.MemoryStream.WriteAsync(ReadOnlyMemory1 buffer, CancellationToken cancellationToken) --- End of stack trace from previous location --- at System.IO.Stream.<CopyToAsync>g__Core|27_0(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken) at System.Net.Http.StreamToStreamCopy.<CopyAsync>g__DisposeSourceAsync|1_0(Task copyTask, Stream source) at System.Net.Http.HttpContent.<CopyToAsync>g__WaitAsync|56_0(ValueTask copyTask) at System.Net.Http.MultipartContent.SerializeToStreamAsyncCore(Stream stream, TransportContext context, CancellationToken cancellationToken) at System.Net.Http.HttpContent.LoadIntoBufferAsyncCore(Task serializeToStreamTask, MemoryStream tempBuffer) at System.Net.Http.HttpContent.<WaitAndReturnAsync>d__822[[System.Net.Http.HttpContent, System.Net.Http, Version=7.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a],[System.Byte[], System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext() at System.Net.Http.BrowserHttpHandler.CallFetch(HttpRequestMessage request, CancellationToken cancellationToken, Nullable1 allowAutoRedirect) at System.Net.Http.BrowserHttpHandler.<SendAsync>g__Impl|55_0(HttpRequestMessage request, CancellationToken cancellationToken, Nullable1 allowAutoRedirect) at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.g__Core|5_0(HttpRequestMessage request, CancellationToken cancellationToken) at Microsoft.AspNetCore.Components.WebAssembly.Authentication.AuthorizationMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) at Microsoft.Extensions.Http.Logging.LoggingScopeHttpMessageHandler.g__Core|5_0(HttpRequestMessage request, CancellationToken cancellationToken) at System.Net.Http.HttpClient.g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken) at VisionServiceWasm.Pages.DatasetManager.UploadSingleFile(IBrowserFile file) at VisionServiceWasm.Pages.DatasetManager.OnUploadFiles() at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task) at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

Upvotes: 3

Views: 1073

Answers (2)

Charlieface
Charlieface

Reputation: 72040

EDIT: The below seems to be only relevant for large responses, not large requests. Will leave here in case it's useful.


This has been documented in a GitHub issue already. It's also poorly documented here.

You need to make two changes:

  • Use HttpCompletionOption.ResponseHeadersRead.
  • Use SetBrowserResponseStreamingEnabled(true).

Both of these mean you need to do this as a full HttpRequestMessage and SendAsync, rather than using PostAsync.

You are also missing a couple of using.

private async Task<FileModel?> UploadSingleFile(IBrowserFile file)
{
    using var content = new MultipartFormDataContent();
    using Stream fileStream = file.OpenReadStream(file.Size);
    using var fileContent = new StreamContent(fileStream);
    fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType);
    fileContent.Headers.ContentLength = fileStream.Length;
    content.Add(fileContent, file.Name);

    using var message = new HttpRequestMessage(HttpMethod.Post, $"File/{_selectedDataset.ID}");
    message.Content = content;
    message.SetBrowserResponseStreamingEnabled(true);

    using var response = await HttpClient.SendAsync(message, HttpCompletionOption.ResponseHeadersRead);

    if (response.IsSuccessStatusCode)
    {
        var addedFile = (await response.Content.ReadFromJsonAsync<IEnumerable<FileModel>>())?.FirstOrDefault();
        return addedFile;
    } // else do something

    return null;
}

Consider also using a CancellationToken to allow the client to efficiently cancel the request.

Upvotes: 1

Aboulfazl Hadi
Aboulfazl Hadi

Reputation: 82

Because you mentioned in this case the server is never event hit, so the problem should be solved in client-side.you can handle uploading huge files by chunking them in the browsers. There are some javascript library for doing that. you can check the following project:

https://github.com/Buzut/huge-uploader

Upvotes: 1

Related Questions