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

2

8

I'm building an ASP.NET Core 6.0 web API. The API has endpoints that take in multipart/form-data requests and save the sections into files. If the internet connection gets cut during the handling of the request the following error is logged into the application's console:

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

After the connection is restored, new requests from the same machine used to send the failed request are not handled by the application unless the application is restarted. This happens for all API endpoints, not just for the failed ones. Postman requests from localhost go through as they should.

My question is: what causes the API to get stuck this way? I don't understand why and how the loss of connection causes the application to stop receiving new requests from the remote machine.

Here is the code I'm using to handle the multipart, this function is called in the controller for the multipart POST requests. It goes through the multipart sections and calls ProcessStreamedFile for each of them. It has other functions as well that I can not share here but nothing related to IO or HTTP communication.

[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();
}

Please ignore the nested try-catch from the code above, there is a reason for it I had to omit it from the code displayed. Below is the code for the 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;
}

The row that is referenced in the error (C:\AppName\Utilities\FileHelpers.cs:line 153) is await section.Body.CopyToAsync(memoryStream, ct);.

I've tried adding the CancellationToken hoping for it to correctly handle the cutting of the request, manually closing the HttpContext with HttpContext.Abort() and HttpContext.Session.Clear(). None of these changed the behavior in any way.

Ochlophobia answered 29/3, 2022 at 8:9 Comment(3)
Side notes: // Check if the file is empty or exceeds the size limit. if (memoryStream.Length == 0) nice example of how comment almost directly go out of sync with the actual code. Also the name memoryStream is a bot odd for a FileStreamComeuppance
Has anyone else ran into this issue and have another solution for it?Barathea
This is the Github issue: github.com/dotnet/aspnetcore/issues/26278 . It's been closed but only because it has transmogrified into another issue (link in original issue).Overexcite
O
5

Solution

This issue was caused by a port-forwarding I was using to make the connection. Due to our network configuration, I had to initially use a Putty tunnel and forward the remote machine's (the one sending the request) port to my local computer (running the server). Somehow this tunnel gets stuck when the connection is lost. Now I was able to change our network so that I can send the request directly to my local machine by using the actual public IP and everything works well.

I am not sure why the Putty tunnel gets stuck but as of now I am able to avoid the problem and can't dig deeper due to time constraints.

Ochlophobia answered 4/4, 2022 at 8:27 Comment(0)
B
2

This cryptic message can be caused by so many things. The reason I got this error was because my Azure WebApp was periodically disconnecting due to lack of processing / memory power. So my solution was to simply upgrade my Azure App-Service Provider (ASP) to the next pricing tier.

Barathea answered 12/7, 2022 at 21:53 Comment(1)
Nice try Satya Nadella!Annelid

© 2022 - 2024 — McMap. All rights reserved.