ASP.NET CORE "BadHttpRequestException: Unexpected end of request content." 导致以后的连接卡住

ASP.NET CORE "BadHttpRequestException: Unexpected end of request content." causes future connections to get stuck

我正在构建一个 ASP.NET Core 6.0 网络 API。 API 具有接收 multipart/form-data 请求并将这些部分保存到文件中的端点。如果在处理请求期间互联网连接被切断,则以下错误将记录到应用程序的控制台中:

Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException: Unexpected end of request content. at Microsoft.AspNetCore.Server.Kestrel.Core.BadHttpRequestException.Throw(RequestRejectionReason reason) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.Http1ContentLengthMessageBody.ReadAsyncInternal(CancellationToken cancellationToken) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpRequestStream.ReadAsyncInternal(Memory 1 buffer, CancellationToken cancellationToken) at Microsoft.AspNetCore.WebUtilities.BufferedReadStream.EnsureBufferedAsync(Int32 minCount, CancellationToken cancellationToken) at Microsoft.AspNetCore.WebUtilities.MultipartReaderStream.ReadAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken) at System.IO.Stream.CopyToAsyncInternal(Stream destination, Int32 bufferSize, CancellationToken cancellationToken) at AppName.Utilities.FileHelpers.ProcessStreamedFile(MultipartSection section, ContentDispositionHeaderValue contentDisposition, IConfiguration conf, ModelStateDictionary modelState, CancellationToken ct) in C:\AppName\Utilities\FileHelpers.cs:line 153

连接恢复后,除非重新启动应用程序,否则应用程序不会处理来自用于发送失败请求的同一台机器的新请求。这发生在所有 API 个端点上,而不仅仅是失败的端点。来自 localhost 的 Postman 请求会正常通过。

我的问题是:是什么导致 API 以这种方式卡住?我不明白连接丢失为什么以及如何导致应用程序停止接收来自远程机器的新请求。

这是我用来处理多部分的代码,在控制器中为多部分 POST 请求调用此函数。它遍历多个部分并为每个部分调用 ProcessStreamedFile。它还有其他功能,我不能在这里分享,但与 IO 或 HTTP 通信无关。

[RequestFormLimits(ValueLengthLimit = int.MaxValue, MultipartBodyLengthLimit = int.MaxValue)]
private async Task<ActionResult> ReadAndSaveMultipartContent()
{
    try
    {
        var boundary = Utilities.MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType),MaxMultipartBoundaryCharLength);

        var cancellationToken = this.HttpContext.RequestAborted;
        var reader = new MultipartReader(boundary, HttpContext.Request.Body);
        var section = await reader.ReadNextSectionAsync(cancellationToken);

        while (section != null)
        {
            try
            {
                var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);

                if (hasContentDispositionHeader)
                {
                    // This check assumes that there's a file
                    // present without form data. If form data
                    // is present, this method immediately fails
                    // and returns the model error.
                    if (!Utilities.MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
                    {
                        ModelState.AddModelError("File", $"The request couldn't be processed (Error 2).");
                        return BadRequest(ModelState);
                    }
                    else
                    {
                        var streamedFilePath = await FileHelpers.ProcessStreamedFile(
                                section, contentDisposition, Startup.Configuration, ModelState,
                                cancellationToken);

                        if (streamedFilePath == "-1")
                        {
                            return BadRequest();
                        }
                            
                        /* MORE CODE HERE */

                            
                }
                else
                {
                    // We go here if contentDisposition header is missing.
                    return BadRequest();
                }
            }
            catch (Exception ex)
            {
                return BadRequest();
            }
            // Drain any remaining section body that hasn't been consumed and
            // read the headers for the next section.
            section = await reader.ReadNextSectionAsync(cancellationToken);
        }
    } catch (Exception ex)
    {
        return BadRequest("Error in reading multipart request. Multipart section malformed or headers missing. See log file for more details.");
    }
    return Ok();
}

请忽略上面代码中的嵌套 try-catch,我不得不从显示的代码中省略它是有原因的。下面是 ProcessStreamedFile.

的代码
public static async Task<string> ProcessStreamedFile(MultipartSection section, Microsoft.Net.Http.Headers.ContentDispositionHeaderValue contentDisposition,IConfiguration conf, ModelStateDictionary modelState, CancellationToken ct)
{
    var completeFilepath = GetFilepath(section, contentDisposition, conf);
    var dirPath = Path.GetDirectoryName(completeFilepath);Directory.CreateDirectory(dirPath);
    try
    {
        using var memoryStream = new FileStream(completeFilepath, FileMode.Create);
        await section.Body.CopyToAsync(memoryStream, ct);

        // Check if the file is empty or exceeds the size limit.
        if (memoryStream.Length == 0)
        {
            modelState.AddModelError("File", "The file is empty.");
            memoryStream.Close();
        }
        else
        {
            memoryStream.Close();
            return completeFilepath;
        }
    }
    catch (Exception ex)
    {
        return "-1";
    }
    return completeFilepath;
}

错误 (C:\AppName\Utilities\FileHelpers.cs:line 153) 中引用的行是 await section.Body.CopyToAsync(memoryStream, ct);

我尝试添加 CancellationToken,希望它能够正确处理请求的切割,手动关闭 HttpContextHttpContext.Abort()HttpContext.Session.Clear()。 None 其中以任何方式改变了行为。

解决方案

这个问题是由我用来建立连接的 port-forwarding 引起的。由于我们的网络配置,我不得不最初使用 Putty 隧道并将远程机器(发送请求的机器)的端口转发到我的本地计算机(运行 服务器)。当连接丢失时,这条隧道会以某种方式卡住。现在我能够更改我们的网络,以便我可以使用实际的 public IP 将请求直接发送到我的本地计算机,并且一切正常。

我不确定为什么 Putty 隧道会卡住,但到目前为止我能够避免这个问题并且由于时间限制无法深入挖掘。