我们有一个uploading large files with streaming的.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在发送整个请求正文之前不会读取响应,但是服务器已经停止处理此请求。那么有没有办法处理这种情况?
答案 0 :(得分:0)
这可能会或可能不会解决问题,但是值得尝试。尝试将SendAsync方法包装在using块中,然后从那里解析响应。另外,请注意您如何解析响应。
using (var response = await _httpClient.SendAsync(request, cancellationToken))
{
var responseContent = await response.Content.ReadAsStringAsync();
// Do whatever with response content ...
}
此外,推荐的管理HttpClient实例的方法是通过IHttpClientFactory
。我不会详细介绍其用法,但是您可能会考虑检查文档,因为仅通过简单地新建一个HttpClient对象就会遇到问题。
答案 1 :(得分:0)
我在MultipartContent
类内的https://www.nginx.com/resources/wiki/modules/auth_digest/方法中找到了错误:
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
甚至都不会尝试读取响应。
因此,我创建了自己的SerializeToStreamAsync实现。它与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();