10

I tried 400 combinations of syntaxes and headers, I can't figure out how to make a HTTP call from Angular to retrieve a file from my NodeJS server.

Found on Stackoverflow and tried, to no avail :

Download file from http post request - Angular 6

How download a file from HttpClient

Download a file from NodeJS Server using Express

How do I download a file with Angular2

It can't be a simple <a download> tag, or a public express.static() folder, because access to the file is restricted and I need to pass a JWT token along (in Node, I have an Express authentication middleware that will reject the request if no token is provided in the headers or if it is invalid).

The file is a GZIP : ./dumps/dump.gz and weighs 812 Kb.

I do manage to download the file, but whatever I try, it weighs 1.4 MB or 94 bytes (wrong size) and can't be opened (7zip can't open file downloads/dump.gz as archive).

What I have tried Angular-side (multiple attempts) :

import { saveAs } from 'file-saver';

let headers = new Headers({
    "Authorization": "Bearer " + user.jwt, // I need this in the headers

    "Content-Type" : "application/octet-stream", // Tried with and without, "application/gzip", "application/json", no difference

    "responseType": "blob" as "json", // Tried with and without, "text", "json", no difference

    "Access-Control-Expose-Headers" : "Content-Disposition" // Tried with and without, no difference
})

this.http
    .get("/download/dump", { headers })
    .toPromise()
    .then(res => {

        const blob = new Blob([res["_body"]] , { type: "application/octet-stream;"} );  // Error : body is not a blob or an array buffer
        // const blob = new Blob([res["_body"]]); // Same result
        // const blob = new Blob([res.blob()]); // Error : body is not a blob or an array buffer

        saveAs(blob, "dump.gz"); // Saves a big corrupted file

        // window.URL.createObjectURL(new Blob(blob, {type: 'blob'})); Saves a 94 byte corrupted file. Tried {type: 'gzip'}, same thing
    })
    .catch(err => console.error("download error = ", err))

What I have tried Node-side (multiple attempts) :

EDIT

Node has been innocented as I could retrieve the file directly from Chrome after disabling authentication. So, the back-end works and the issue is in Angular.

app.get( "/download/dump", authenticate, (req:Request, res:Response) => {
    const file = path.resolve(__dirname, `./dumps/dump.gz`);

    res
        .set({ // Tried with and without headers, doesn't seem to do anything
            "Content-Disposition" : "attachment",  // Tried with and without
            "filename" : "dump.gz", // Tried with and without
            "filename*" : "dump.gz",  // Tried with and without
            "Content-Encoding" : "gzip",  // Tried with and without
            "Content-Type" : "application/gzip"  // Tried with and without, "application/text", "application/json", no difference 
        })
        .sendFile(file); // getting a big corrupted file
        // .download(file); // Same result (big corrupted file)
})
Jeremy Thille
  • 26,047
  • 12
  • 43
  • 63
  • did you try using postman to serve the file and see if your backend it working fine? – Arghya Saha May 24 '19 at 06:16
  • I don't get what you mean. The backend serves the file, and Postman is a tool to query the backend and _retrieve_ the file from it, isn't it? Not to serve it. It's the other way round, or what did I miss? – Jeremy Thille May 24 '19 at 06:57
  • 1
    Please test your backend code first. Whether your backend is able to send the file properly. Then look at your frontend angular code – Arghya Saha May 24 '19 at 07:25
  • This is exactly my problem. Is the back-end not sending the file properly? Or is Angular not receiving it properly? Because as far as I can tell, in the console, Node does find the file and send something, and in the browser, I do see data coming from the server. So, "something" is being sent from server to browser. But then, what is wrong? Probably some header or encoding somewhere, but I can't figure out which. – Jeremy Thille May 24 '19 at 08:42
  • Do you get any warning in the console (cors or anything)? Can you deactivate the JWT check and just open the url in a browser to see if you can download a valid file? – David May 24 '19 at 10:35
  • No error in the console, no CORS issues. I don't do any cross-domain request. Trying without authentication is a very good idea. I tried and could indeed fetch the file correctly with Chrome with a simple GET request. I can conclude the back-end works well and the issue is in Angular :) We're making progress! Thanks! – Jeremy Thille May 24 '19 at 12:16
  • What if you just specify `responseType: 'blob'` and `Authorization` in your headers? `Access-Control-Expose-Headers` is a server side header, and `"blob" as "json"` looks odd – David May 24 '19 at 12:36
  • `What if you just specify responseType: 'blob' and Authorization in your headers?` --> The file is downloaded, but is 1.4 MB instead of 812 Kb and can't be opened. `"blob" as "json"` is a trick I found on several blogs and articles, saying it was indeed an odd trick but was necessary for Typescript to stop complaining. Example [here](https://medium.com/techinpieces/blobs-with-http-post-and-angular-5-a-short-story-993084811af4) – Jeremy Thille May 24 '19 at 12:48
  • 2 things: 1. I know you said it works, but I tried using your backend code and I don't get a proper file with all the headers you specified. Without any server header, I get a proper file from the backend. 2. Which version of angular are you using? And which http client? `HttpModule` or `HttpClientModule`? The `Headers` class is only for the old, deprecated `HttpModule `deprecated – David May 24 '19 at 13:19
  • I'm still using the old `Http` module... This application is now two years old, I'm maintaining it. I think you found something here, I'll try with the new `HttpClientModule` – Jeremy Thille May 24 '19 at 15:05

1 Answers1

15

Assuming that you are using the new HttpClient from angular (available since angular 4), this should work

front

import { saveAs } from 'file-saver';
import {HttpHeaders} from "@angular/common/http";

let headers = new HttpHeaders({
    "Authorization": "Bearer " + user.jwt, // Auth header
    //No other headers needed
});

this.http
    .get("/download/dump", { headers, responseType: "blob" }) //set response Type properly (it is not part of headers)
    .toPromise()
    .then(blob => {
        saveAs(blob, "dump.gz"); 
    })
    .catch(err => console.error("download error = ", err))

backend

app.get( "/download/dump", authenticate, (req:Request, res:Response) => {
    const file = path.resolve(__dirname, `./dumps/dump.gz`);
    //No need for special headers
    res.download(file); 
})
David
  • 33,444
  • 11
  • 80
  • 118
  • `{ headers, responseType: "blob" }` isn't a valid syntax, I think it should be `{ headers: headers, responseType: "blob" }`. But Typescript points out : `(property) RequestOptionsArgs.responseType?: ResponseContentType Type 'string' is not assignable to type 'ResponseContentType'.ts(2322)` and refuses to compile – Jeremy Thille May 24 '19 at 15:03
  • I tried that code at work and it worked. I don't think there was any warning about the syntax, but I was using TS 3.4. The message about `ResponseContentType` is definitely because you are using the old `HttpModule` – David May 24 '19 at 17:03
  • Using the new HttpClient fixed it. I needed some time to update my app because it broke everything, but now it works. Not sure why it was impossible with the old Http module though... but at least the new one works. Thanks @David, here's your bounty reward :) – Jeremy Thille May 27 '19 at 12:26
  • Thanks :) Yeah it's probably possible using the old one as well, but you're better off with the new one I'm sure – David May 27 '19 at 12:38