Search code examples
node.jsangularexpressfilesaver.js

How to download an excel (xlsx) file using Angular 5 HttpClient get method with Node/Express backend?


I have an excel file in a directory on my nodejs server - Path to the file is - ./api/uploads/appsecuritydesign/output/appsecdesign.xlsx

On click of a button in my Angular 5 component I am just trying to download the file using FileSaver.

Below is my Angular component.

Angular Component with Download button

Here the template code for the button in Angular that will call the saveFile() function once clicked.

<a class="btn btn-primary" (click) = "saveFile()">Download</a>

Here is the saveFile() function.

   saveFile(){

    console.log("In ToolInput Component: ", this.token); //Okta token
          console.log("In ToolInput Component: ", this.whatamidoing); //this variable has the server FilePath

this.fileService.getappsecdesignfile(this.token, this.whatamidoing).subscribe(res => {
            let blobtool5 = new Blob([res], { type: 'application/vnd.ms-excel;charset=utf-8' });
            FileSaver.saveAs(blobtool5, 'Application_Security_Design.xlsx');
          },
          (err: HttpErrorResponse) => {
            if (err.error instanceof Error) {

                  console.log('An error occurred:', err.error.message);
                  console.log('Status', err.status);
                } else {
                  console.log(`Backend returned code ${err.status}, body was: ${err.error}`);
                  console.log('Status', err.status);
                }
          });
    }

At this point I checked the console.log in the browser. They are exactly what they are supposed to be. So I am passing the correct filepath and token to getappsecdesignfile method in my fileService.

enter image description here

Now Lets take a look at the getappsecdesignfile method in my fileService.

getappsecdesignfile ( token, tool5filepath ) : Observable<any>{

       console.log("In Service tool5filepath: ", tool5filepath);
       console.log("In Service token", token);
       console.log("In Service GET url: ", this.getappsecdesignfileurl);

       //Since the tool5filepath has / (slashes etc) I am encoding it below.

       let encodedtool5filepath = encodeURIComponent(tool5filepath);
       console.log('Encoded File Path: ', encodedtool5filepath);

       let req = new HttpRequest('GET', this.getappsecdesignfileurl,{params: new HttpParams().set('path', encodedtool5filepath)},{headers: new HttpHeaders().set('Accept', 'application/vnd.ms-excel').set('Authorization', token)});
       console.log(req);
       return this.http.request(req);

   }

That's all there is to the fileService method. Lets look at the console.logs from this method from the browser to ensure all the correct values are being set.

enter image description here

Now Lets take a look at the request itself before we go to the server part.

enter image description here

As far as I am concerned the headers are set correctly, params are set correctly. Two issues I see is that Angular's interceptors probably sets the responseType: json and adds a param op:s to my request.

Node/Express Server code.

app.get('/getappsecdesignfile', function(req, res){
console.log("In get method app security design");
    accessTokenString = req.headers.authorization;
    console.log("Okta Auth Token:", accessTokenString);

    console.log("Tool5 File Path from received from Angular: ", req.query.path); //this is where the server console logs shows Tool5 File Path after decoding: ./undefined

    oktaJwtVerifier.verifyAccessToken(accessTokenString)
    .then(jwt => {
    // the token is valid
      console.log(jwt.claims);

      res.setHeader('Content-Disposition', 'attachment; filename= + Application_Security_Design.xlsx');
      res.setHeader('Content-Type', 'application/vnd.ms-excel');

      let tool5filepath = './' + decodeURIComponent(req.query.path);
      console.log("Tool5 File Path after decoding: ", tool5filepath);
      res.download(tool5filepath);

      }).catch(err => {
        // a validation failed, inspect the error
        res.json({success : false, message : 'Authorization error.'});
      });
    });

If I use Postman the api works fine. However somewhere between Angular to Node communication something happens that I don't understand.

Below is what the server logs. (Big question how does this become undefined)?

Tool5 File Path from received from Angular:  undefined
Tool5 File Path after decoding:  ./undefined
Error: ENOENT: no such file or directory, stat '<dirpath>/undefined'

Here is what I see in the browser log:

zone.js:2933 GET http://localhost:3000/getappsecdesignfile 404 (Not Found)
toolinput.component.ts:137 Backend returned code 404, body was: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>Error: ENOENT: no such file or directory, stat &#39;<dirpath>/undefined&#39;</pre>
</body>
</html>

Then the browser downloads a xlsx file that is corrupt and cannot be opened.

I have checked the file resides in the directory and is ready to be downloaded.

enter image description here

Thanks for any tips that can help me resolve this issue.


Solution

  • Finally figured it out.

    2 specific changes made this work.

    Change # 1 - Setting responseType : 'blob' and defining the params and headers first and then using them in http.get. (http is nothing but an object of type HttpClient from angular/common/http that has been injected into the service class.

    getappsecdesignfile ( token, tool5filepath ) : Observable<any>{
    
           console.log("In Service tool5filepath: ", tool5filepath);
           console.log("In Service token", token);
           console.log("In Service GET url: ", this.getappsecdesignfileurl);
    
           let encodedtool5filepath = encodeURIComponent(tool5filepath);
           console.log('Encoded File Path: ', encodedtool5filepath);
    
           let getfileparams = new HttpParams().set('filepath', encodedtool5filepath);
           let getfileheaders = new HttpHeaders().set('Accept', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet').set('Authorization', token);
    
           return this.http.get(this.getappsecdesignfileurl, {responseType: 'blob', params: getfileparams, headers: getfileheaders});
       }
    

    Change # 2 - Component code - FileSaver. For some reason type: 'application/vnd.ms-excel' did not work in FileSaver. Here the res is nothing but the response from the http.get call.

    let blobtool5 = new Blob([res], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
    FileSaver.saveAs(blobtool5, 'Application_Security_Design.xlsx');