c# - C#。如何在仍然上传大型请求正文的同时处理服务器响应?
问题描述
我们有一个 .net 核心 Web API,用于通过流上传大文件:
[HttpPost("file")]
[DisableFormValueModelBinding]
[RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = long.MaxValue)]
public async Task<IActionResult> PostFile()
{
// parse multipart request body
var reader = new MultipartReader(boundary, HttpContext.Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
// file
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
var fileName = contentDisposition.FileName.Value;
var contentType = section.ContentType;
// extension whitelist
string extension = Path.GetExtension(fileName).Trim('.');
if (!fileExtensionWhitelist.Select(_ => _.Extension).Any(s => s.Equals(extension, StringComparison.OrdinalIgnoreCase)))
{
return BadRequest("Forbidden file extension")
}
var fileStream = section.Body;
// upload file
}
// other form data
else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
{
// other data
}
}
section = await reader.ReadNextSectionAsync();
}
return Ok(fileId);
}
现在我正在尝试实现用于上传文件的客户端代码。
using (var httpClient = new HttpClient())
using (var fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read))
{
var content = new MultipartFormDataContent();
content.Add(new StreamContent(fileStream, 5242880/*5MB*/)
{
Headers =
{
ContentLength = fileStream.Length,
ContentType = new MediaTypeHeaderValue(MimeMapping.GetMimeMapping(fileName)),
}
}, "file", fileName);
using (var request = new HttpRequestMessage())
{
request.Content = content;
request.Method = new HttpMethod("POST");
request.Headers.Accept.Add(System.Net.Http.Headers.MediaTypeWithQualityHeaderValue.Parse("text/plain"));
request.RequestUri = new Uri(_url);
var response = await httpClient.SendAsync(request, cancellationToken);
// parse response
}
}
因此,在我尝试上传带有禁止扩展名的文件之前,一切都按预期工作:服务器检测到此扩展名并返回 BadRequest 对象。但客户端仍在尝试上传多部分请求(文件最大可达 1GB)。它导致了这个异常:
该流不支持并发 IO 读取或写入操作
我知道发生这种情况是因为 HttpClient 在发送整个请求正文之前不会读取响应,但是服务器已经停止处理此请求。那么有没有办法处理这种情况呢?
解决方案
我将错误追踪到类中的SerializeToStreamAsync方法MultipartContent
:
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
// other code
TaskCompletionSource<Object> localTcs = new TaskCompletionSource<Object>();
// somewhere here we get the error
// it results in setting the exception on the task
localTcs.TrySetException(ex);
return localTcs.Task;
}
此异常HttpClient
表明请求失败。所以HttpClient
甚至不尝试阅读响应。
所以我创建了自己的MultipartFormDataContent实现。它与 Microsoft 的实现相同,但具有重写SerializeToStreamAsync
方法:
public class CustomMultipartFormDataContent : MultipartContent
{
// implementation from https://github.com/microsoft/referencesource/blob/master/System/net/System/Net/Http/MultipartFormDataContent.cs
protected override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
TaskCompletionSource<object> tcs = new TaskCompletionSource<object>();
Task task = base.SerializeToStreamAsync(stream, context);
task.ContinueWith(copyTask =>
{
if (copyTask.IsFaulted)
{
var exception = copyTask.Exception.GetBaseException();
if (exception.GetType().Equals(typeof(NotSupportedException)))
{
// We assume that our multipart request is usually large, as we want to send large files over http.
// The nature of http communication is: we send the whole request to the server and only then reads the response.
// But the server can abort the request and send the response, while we are still sending large request.
// This situatuion leads to concurrent read/write operations on the http stream which leads to NotSupportedException.
// Here we try to catch this exception, abort sending of the request and start reading the response.
tcs.TrySetResult(null);
}
else
{
tcs.TrySetException(exception);
}
}
else if (copyTask.IsCanceled)
{
tcs.TrySetCanceled();
}
else
{
tcs.TrySetResult(null);
}
},
CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
return tcs.Task;
}
} // CustomMultipartFormDataContent
当代码捕获NotSupportedException
时,它不会在任务上设置异常。在这种情况下,HttpClient
看起来请求已成功发送,它将尝试读取响应。
以及客户端代码的更改:
// var content = new MultipartFormDataContent(); <- old code
var content = new CustomMultipartFormDataContent();
推荐阅读
- kubernetes-helm - 在 k8s 上使用 helm 安装 Artifactory 时 CA 签名证书
- html - 如何制作滚动链接
- swift - 使用 IAP 订阅的正确方法
- c# - 通过在 C# 中使用 Moq 模拟外部 DLL 进行单元测试
- c# - 即使输入有效,错误提供程序也不会自行清除
- mysql - 重新加载页面时获取 JSON 对象
- node.js - 通过 Vue CLI 添加 Vuetify 会导致“找不到模块”错误
- python - 使用 Pandas 逐年获取值的总和
- node.js - React App 的 Heroku 部署问题 - 找不到模块
- excel - Excel VBA 根据名称过滤多个工作表并另存为单独的文件