9

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.

3
  • 1
    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 FileStream Commented Mar 29, 2022 at 8:20
  • Has anyone else ran into this issue and have another solution for it? Commented Jul 11, 2022 at 0:37
  • 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). Commented Jun 17, 2024 at 11:59

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

Comments

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.

1 Comment

Nice try Satya Nadella!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.