Search code examples
restasp.net-corepostfile-uploadconnection

Handle uncompleted file upload by API POST endpoint in asp.net core MVC


To simplify the problem let's say I have a simple asp.net mvc endpoint which receives a file. In most of the cases it will be a .jpg one:

[HttpPost]
[Route("appraisal/{appraisalID}/files/{fileSubjectCode}")]
[ProducesResponseType(StatusCodes.Status201Created, Type = typeof(IEnumerable<AppraisalFileModel>))]
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(ModelStateDictionary))]
public async Task<IActionResult> UploadAppraisalFile(int appraisalID, string fileSubjectCode, [FromForm]IFormFile file)
{
        file = file ?? Request.Form.Files?.FirstOrDefault();

        // intermitent code change to investigate and validate Complete File Size and Actual File Size
        var completeFileSizeHeader = Request.Headers["Complete-File-Size"];
        int.TryParse(completeFileSizeHeader, out int completeFileSize);

        if (file == null || file.Length != completeFileSize)
        {
            using (var stream = new MemoryStream())
            {
                await file.CopyToAsync(stream);
                stream.Position = 0;

                var inputAsString = Convert.ToBase64String(stream.ToArray());
                Logger.LogDebug("Complete-File-Size header doesn't much received byteArray size", file.Length, completeFileSize, inputAsString);
            }
            return StatusCode(StatusCodes.Status411LengthRequired, "Complete-File-Size header doesn't much received byteArray size");
        }


        // some other logic..
}

I'm trying to prevent an edge case when somebody performs a POST request against my API UploadAppraisalFile endpoint and suddenly loses an internet connection which would result in sending a request with not the full file content. My idea was to count the file size at the point where the file is uploaded, add the information about the file size as an extra HTTP-HEADER (I called it Complete-File-Size), and then when the request reaches the backend, count if the received file size is exactly the same as the Complete-File-Size.

To produce such an issue/edge case I tried:

  • uploading a big file(about 16MB) and then suddenly after submitting the HTML form immediately close the browser window.
  • uploading a file and then, in the Chrome browser, in the Network pane, change the uploading speed to a very minimum, then submit the form and then immediately close the browser.

When I run the debug mode, in each case I found that either: UploadAppraisalFile endpoint was never reached or if it was reached then always the full file was sent. For the 2nd successful case, to be 100% sure I converted the received file into base64 string and then I checked how the file looks like in https://codebeautify.org/base64-to-image-converter.

My question is: Is it even possible that the sent POST request is broken and contains not full file content due to a broken internet connection that happened suddenly during the sending process? If yes, then what's the best way to produce the issue. Cheers


Solution

  • You can pass HttpContext.RequestAborted as a CancellationToken to ALL async methods provided by .NET in "some other logic" part.

    Let's use code you provided as an example :

    await stream.CopyToAsync(memoryStream, HttpContext.RequestAborted)
    

    I don't have an access to a full method but I assume you save it to some blob storage or file system. Most of these persistence API's accept CancellationToken as a parameter.

    Receiving incomplete file

    I was able to achieve a "partial" file using this code and Postman. It will basically read chunks from response stream until connection is interrupted. As soon as I close Postman window TaskCancelledException is raised and stream is closed.

    [HttpPost]
    public async Task<IActionResult> UploadAppraisalFile([FromForm] IFormFile file)
    {
        var appraisalfile = file ?? Request.Form.Files.FirstOrDefault();
    
        if (appraisalfile != null)
        {
            var buffer = ArrayPool<byte>.Shared.Rent(1024);
            using var stream = appraisalfile.OpenReadStream();
            while (await stream.ReadAsync(buffer, 0, buffer.Length, HttpContext.RequestAborted) > 0)
            {
                 // Do something with buffer
                 _logger.LogInformation("Total length (bytes) {0}, partial length (bytes) {1}", stream.Length, stream.Position);
            }
            ArrayPool<byte>.Shared.Return(buffer);
        }
    
        return Ok();
    }