C#。如何在仍然上传大型请求正文的同时处理服务器响应?

时间:2020-10-14 19:04:49

标签: c# http .net-core

我们有一个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在发送整个请求正文之前不会读取响应,但是服务器已经停止处理此请求。那么有没有办法处理这种情况?

2 个答案:

答案 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();
相关问题