如何防止 HttpClient.PostAsync 占用过多内存?

huangapple go评论143阅读模式
英文:

How to prevent HttpClient.PostAsync to fill up memory?

问题

我在我的Blazor WebAssembly应用程序中有以下函数,用于将大文件上传到REST服务器:

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;
}

上述函数应该能够将大文件流式传输到API服务器。它可以正常处理中小型文件,但是在测试大文件时,在客户端端会出现内存溢出异常。甚至在到达服务器端点之前,HttpClient.PostAsync()方法就会填充客户端内存,尽管我似乎没有将流复制到其他地方。

我已经尝试过使用带有TransferEncodingChunked = true头的HttpRequestMessageSendAsync(),但是得到了相同的结果。在幕后的某个地方,整个数据流被复制到内存中。我该如何避免这种情况?

英文:

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

private async Task&lt;FileModel?&gt; 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($&quot;File/{_selectedDataset.ID}&quot;, content);

    if (response.IsSuccessStatusCode)
    {
        var addedFile = (await response.Content.ReadFromJsonAsync&lt;IEnumerable&lt;FileModel&gt;&gt;())?.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.&lt;CopyToAsync&gt;g__Core|27_0(Stream source, Stream destination, Int32 bufferSize, CancellationToken cancellationToken)
at System.Net.Http.StreamToStreamCopy.&lt;CopyAsync&gt;g__DisposeSourceAsync|1_0(Task copyTask, Stream source)
at System.Net.Http.HttpContent.&lt;CopyToAsync&gt;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.&lt;WaitAndReturnAsync&gt;d__82
2[[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.&lt;SendAsync&gt;g__Impl|55_0(HttpRequestMessage request, CancellationToken cancellationToken, Nullable
1 allowAutoRedirect)
at Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler.<SendAsync>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.<SendAsync>g__Core|5_0(HttpRequestMessage request, CancellationToken cancellationToken)
at System.Net.Http.HttpClient.<SendAsync>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)

答案1

得分: 1

因为你提到在这种情况下服务器从未被触发,所以问题应该在客户端解决。你可以通过在浏览器中将大文件分块上传来处理上传大文件的问题。
有一些用于执行此操作的JavaScript库。你可以检查以下项目:

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

英文:

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

答案2

得分: 1

这已经在GitHub问题中有记录。在这里也有一些文档,但文档质量较差。

你需要做两个更改:

  • 使用 HttpCompletionOption.ResponseHeadersRead
  • 使用 SetBrowserResponseStreamingEnabled(true)

这两者意味着你需要将这个操作作为一个完整的 HttpRequestMessageSendAsync,而不是使用 PostAsync

你还遗漏了一些 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;
}

还考虑使用 CancellationToken 允许客户端有效地取消请求。

英文:

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&lt;FileModel?&gt; 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, $&quot;File/{_selectedDataset.ID}&quot;);
    message.Content = content;
    message.SetBrowserResponseStreamingEnabled(true);

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

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

    return null;
}

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

huangapple
  • 本文由 发表于 2023年3月15日 18:27:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/75743440.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定