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.
// 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 namememoryStream
is a bot odd for aFileStream
– Comeuppance