3

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--
ckapilla
  • 1,148
  • 13
  • 25
  • 1
    `Content-Type: application/json` That's your problem right there. You're sending the file as form data, you need to specify the correct content type in your Angular request code. The correct type in this case would be `multipart/form-data`. This is true even when you're calling an API, the standard method for file uploads is to send it as form data like you're doing. Just need to specify the correct content-type. – jdewerth Dec 12 '20 at 09:10
  • thanks -- please see the comment I have added to my original question regarding this – ckapilla Dec 12 '20 at 10:58

2 Answers2

3

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(); 
}
jdewerth
  • 564
  • 3
  • 9
  • thanks for your input, please see my edit to the original question regarding this – ckapilla Dec 12 '20 at 11:13
  • I realized I should accept this as an answer as it does resolve my specific question about the null parameter even though I still have work to do to get the upload working. But now I know where to look. – ckapilla Dec 12 '20 at 14:44
  • @ckapilla Can you provide the rest of your controller action code? It looks like an exception is being thrown at some point within it, I don't think it's anything Kestrel related. – jdewerth Dec 12 '20 at 18:32
  • @ckapilla Also, it looks like you're using the file name as the Key for the file field on the form. You need to use the same key between both your Angular code and the API. Example, `PostStudentGradesReport([FromForm] IFormFile file);` and your Angular code should use the same key, i.e. `frmData.append('file', file);` notice the key change to 'file'. You can get the actual file name of the file in your controller code, the Key does not indicate the file name. – jdewerth Dec 12 '20 at 19:17
  • regarding your first comment, the error is definitely thrown from the internals of Kestrel. If you google it you can see there are a near endless number of reasons for getting this error, but all are inside the Kestrel processing. Also my controller code never actually gets entered when the exception occurs. – ckapilla Dec 12 '20 at 20:48
  • regarding your second comment, that is very helpful (and very obvious once you pointed it out!) I have found it was one of two changes I needed to make. The second is adding a "boundary---------------------xxxxxxxxxxx" clase to the Content-type header. That being missing was the cause of the exception. Now I just have to find out HOW to add that clause.... – ckapilla Dec 12 '20 at 20:52
  • @ckapilla Have you tested it with after changing the form-field key? I saw someone experiencing a boundary error because he wasn't matching the parameter name with the form-field key. You should be seeing an `InvalidDataException` if it's caused by the Multipart boundary issue. It looks like you're using the development exception page, if so what is the specific output there? Your content-length looks to be within the Kestrel limits. – jdewerth Dec 12 '20 at 20:57
  • 1
    yes,using Fiddler, I have tested with the boundary= issue fixed, and that still gave the null parameter issue, but fixed the exception. Then by changing the form-field key per your suggestion, everything worked all the way through. In my program, it looks like I can just specify the boundary= as part of the Content-type and that will get used. – ckapilla Dec 12 '20 at 21:01
  • 1
    @ckapilla Regarding the boundary issue, it turns out I overlooked something and the solution is actually very simple. When you manually specify the content-type header, the boundary values are not automatically calculated and added to the header value. Instead, if you leave it default and do not manually add a `Content-Type` header to your request object, they will be automatically set. I updated my answer to include this additional information as well as a link to another question which points this out. – jdewerth Dec 12 '20 at 21:33
  • wow thanks so much for this last comment -- it actually works and everything else I tried did not work. That post you linked to was magic, but somehow I never found it on my own. – ckapilla Dec 12 '20 at 21:51
1

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?

Coco
  • 176
  • 3
  • Thanks for the suggestion; but alas the data is still not finding its way into the ASPNet controller. Regarding the data validation, you are correct, I just have that in there temporarily in order to prove that frmData is not null on the client side before the call. – ckapilla Dec 12 '20 at 01:10