File Upload from Angular to ASP.NET Core

Viewed 1199

For the first time I am trying to upload a file from an Angular component to an ASPNET Core web page, and simply can't get it to work. Hopefully the following code excerpts will be enough to show the essentials of what is going on. The issue is that although I confirm that the passed parameter to the HttpClient's post method (frmData) is valid, the ASPNet Core action method never sees it, and reports that IFormFile is always null.

EDIT: I had previously tried using multipart/form-data as the content-type but I gave an unhandled exception in the guts of Kestrel . I realize now that this is the correct way to do it, and using json content type was the source of my ORIGINAL problem. But I don't know where to go from here. I see from some googling there are about a \billion different causes for that exception to occur.

POST Executing endpoint 'JovenesA.Controllers.StudentssController.PostStudentGradesReport (JAWebAPI)'
04:55:38.4853 Info ControllerActionInvoker
POST Route matched with {action = "PostStudentGradesReport", controller = "Becas"}. Executing action JovenesA.Controllers.BecasController.PostStudentGradesReport (JAWebAPI)
04:55:38.5032 Error DeveloperExceptionPageMiddleware
POST An unhandled exception has occurred while executing the request.
04:55:38.5333 Info WebHost
POST Request finished in 48.1225ms 500 text/html; charset=utf-8
04:55:38.5333 Info Kestrel
 Connection id "0HM4UHGE85O17", Request id "0HM4UHGE85O17:00000006": the application completed without reading the entire request body.

Any help would be greatly appreciated!

Angular Component:

fileEntry.file((file: File) => {
      console.log('fileEntry relativePath: ' + currFile.relativePath);
      console.log('filEntry.name: ', file.name);
      console.log('filEntry.size: ', file.size);

      const frmData = new FormData();
      frmData.append(file.name, file);

      this.studentData.uploadStudentGradesReport(file.name, frmData).subscribe(
        () => {
          this.successMessage = 'Changes were saved successfully.';
          window.scrollTo(0, 0);
          window.setTimeout(() => {
            this.successMessage = '';
          }, 3000);
        },
        (error) => {
          this.errorMessage = error;
        }
      );
    });

Angular Service:

public uploadStudentGradesReport(filename: string, frmData: FormData): Observable<any> {
    const url = this.WebApiPrefix + 'students/' + 'student-grades-report';
    const headers = new HttpHeaders().set('Content-Type', 'application/json');
    if (frmData) {
      console.log('ready to post ' + url + ' filename: ' + filename + ' options ' + headers);
      return this.http.post(url, frmData, { headers });
    }
}

ASPNET Core Controlle

// POST api/students/student-grades-report
[HttpPost("student-grades-report", Name = "PostStudentGradseReportRoute")]
//[ValidateAntiForgeryToken]
[ProducesResponseType(typeof(GradesGivenEntryApiResponse), 200)]
[ProducesResponseType(typeof(GradesGivenEntryApiResponse), 400)]
public async Task<ActionResult> PostStudentGradesReport([FromForm] IFormFile myFile)
{
    _Logger.LogInformation("Post StudentGradesReport  ");

    if (myFile != null)
    {
        var totalSize = myFile.Length;
        var fileBytes = new byte[myFile.Length];

If it helps, here is the data that is being sent in the POST request

POST http://192.168.0.16:1099/api/students/student-grades-report HTTP/1.1
Host: 192.168.0.16:1099
Connection: keep-alive
Content-Length: 13561
Accept: application/json, text/plain, */*
DNT: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Content-Type: application/json
Origin: http://localhost:3000
Referer: http://localhost:3000/
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9,es-MX;q=0.8,es;q=0.7

------WebKitFormBoundaryBVuZ7IbkjtQAKQ0a
Content-Disposition: form-data; name="test1.PNG"; filename="test1.PNG"
Content-Type: image/png

 PNG
 
  [ binary contents of the image file ]  

------WebKitFormBoundaryBVuZ7IbkjtQAKQ0a--
2 Answers

You're sending the file as form data, so you need to specify the correct content type header. Currently your sending application/json in the Content-Type header. This is true even when calling an API, which can understandably be confusing at first. The correct content type in this case is multipart/form-data. Your API is not seeing the IFormFile because it thinks the request is JSON. I've modified your Angular code with the correct content-type header value.

Edit: Turns out that manually specifying a Content-Type header will cause the boundary values not to be set automatically in the header value. Instead, the simple solution is to not add the header yourself, which will result in the proper content-type and boundary values to be automatically set. If you set the header yourself, you will also have to set the boundary values as well. For most situations, leaving it default is probably the best solution. Link to question/answer which points this out. FormData how to get or set boundary in multipart/form-data - Angular

public uploadStudentGradesReport(filename: string, frmData: FormData): Observable<any> {
    const url = this.WebApiPrefix + 'students/' + 'student-grades-report';
    const headers = new HttpHeaders().set('Content-Type', 'multipart/form-data');
    if (frmData) {
      console.log('ready to post ' + url + ' filename: ' + filename + ' options ' + headers);
      return this.http.post(url, frmData, { headers });
    }
}

You can also note the content-disposition that is on the HTTP request that you provided, which shows form data along with the type of file attached. Hope this helps. I didn't fire up an Angular project to test your code but the content-type should fix your issue.

Edit : I noticed that you're using the file name as the key for the form field with the file. You need to use a key such as just 'file' for the form field, which should match the name of the parameter in your controller code. You can get the actual filename of the file within your controller code, the key simply indicates which form field the file(s) are attached to. Example

frmData.append('file', file);

And then for your controller action

public async Task<IActionResult> PostStudentGradesReport([FromForm] IFormFile file)
{
    if (file.Length <= 0 || file.ContentType is null) return BadRequest();
    var actualFileName = file.FileName;

    using (var stream = file.OpenReadStream())
    {
        // Process file...
    }
    
    return Ok(); 
}

I can't guarantee that this will work, but you can try using Angular's HttpRequest. So in your angular service, try this:

const request = new HttpRequest (
    'POST',
     url, // http://localhost/your_endpoint
     frmData,
     { withCredentials: false }
);
    
return this.http.request(request);

Also note that you shouldn't do data validation in the function that calls backend Api. What is your function gonna return if if(frmData) is false?

Related