34

I've written code that uses Angular $http to download a file. The name of the file is not specified in the URL. The URL contains a unique identifier for the file, which is fetched from outside the application.

When $http.get(myUrl) is called, everything works fine; the file is retrieved and I can access it in my callback handler, but I can't see how to get the name of the file. Capturing the raw response with Fiddler, I see this:

HTTP/1.1 200 OK
Cache-Control: private
Content-Length: 54
Content-Type: application/octet-stream
Server: Microsoft-IIS/8.5
Access-Control-Allow-Origin: http://www.example.com/getFile/12345
Access-Control-Allow-Credentials: true
Content-Disposition: attachment; filename=testfile.txt
X-AspNet-Version: 4.0.30319
X-Powered-By: ASP.NET
Date: Fri, 09 Oct 2015 20:25:49 GMT

Lorem ipsum dolar sit amet!  The contents of my file!

From the above, it is clear that the server is sending back the name of the file in the "Content-Disposition", but I haven't found anyway to access it within my Angular callback. How do I get the name of the file from the headers?

Edit in response to answer below: I should have mentioned before that I already tried response.headers(). It returns Object {content-type: "application/octet-stream", cache-control: "private"}, so I still don't get Content-Disposition for some reason. response.headers('Content-Disposition') returns null.

Vivian River
  • 31,198
  • 62
  • 198
  • 313
  • if `response.headers('Content-Disposition') returns null` there's clearly something wrong. The answer from @andrew works perfect – spankmaster79 Aug 10 '16 at 08:10
  • did you find a solution? I have the exact same problem: Fiddler clearly shows a correct content disposition header, but none of the solutions below work: response.headers('Content-Disposition') returns null – fikkatra Oct 13 '16 at 07:25
  • I was able to make it work in the context of the particular task I was doing, but I don't have a good general answer. – Vivian River Oct 14 '16 at 15:14
  • 6
    if `response.headers('Content-Disposition') returns null` and you are using CORS in your API you need to make sure that you add this header in your response: `access-control-expose-headers:content-disposition` https://stackoverflow.com/a/44504121/2069306 – chris31389 Sep 21 '17 at 10:26
  • Add `Response.Headers.Add(HeaderNames.AccessControlExposeHeaders, "content-disposition");` to the request function and it will give read access to the script for this particular call – Suryaprakash Dec 01 '21 at 14:50

12 Answers12

49

It may be worth mentioning that in order to get the file name from the HTTP headers, extracting the Content-Disposition header is not enough. You still need to obtain the filename property from this header value.

Example of header value returned: attachment; filename="myFileName.pdf".

The function below will extract filename="myFileName.pdf", then extract "myFileName.pdf" and finally remove the extra quotes around to get myFileName.pdf.

You can use the snippet below:

  function getFileNameFromHttpResponse(httpResponse) {
      var contentDispositionHeader = httpResponse.headers('Content-Disposition');
      var result = contentDispositionHeader.split(';')[1].trim().split('=')[1];
      return result.replace(/"/g, '');
  }
Andrew
  • 7,848
  • 1
  • 26
  • 24
  • 2
    You might have to adapt the first line of the function provided by @andrew, depending on your Angular version. `var contentDispositionHeader = httpResponse.headers.get('Content-Disposition')` – Tiberiu Oprea Sep 19 '18 at 10:04
29

Web API: I found that adding the following line of code into the ExecuteAsync(...) method of my IHttpActionResult implementation worked ('response' is the HttpResponseMessage to be returned):

response.Content.Headers.Add("Access-Control-Expose-Headers", "Content-Disposition");

Angular: I was then able to resolve the filename in angular as follows ('response' is the resolved promise from $http.get):

var contentDisposition = response.headers('Content-Disposition');
var filename = contentDisposition.split(';')[1].split('filename')[1].split('=')[1].trim();
Phil
  • 1,062
  • 1
  • 17
  • 15
  • 1
    You still need to remove extra `""` from the file name with `.replace(/"/g, '')`: ` let contentDisposition = response.headers('Content-Disposition'); let filename = contentDisposition.split(';')[1].split('filename')[1].split('=')[1].replace(/"/g, '').trim();` – serge.k May 10 '19 at 09:10
  • Although you might be right with the comment, this was the solution to the original problem and thus a valid answer in that context! – David Zwart Feb 01 '20 at 21:18
  • 1
    for .NET Core `app.Use((ctx, next) => { ctx.Response.Headers.Add("Access-Control-Expose-Headers", "*"); ` – Cyclion Aug 03 '20 at 08:04
16

If you use CORS, you need to add the "Access-Control-Expose-Headers" to the response headers at server side. For example: Access-Control-Expose-Headers: x-filename, x-something-else

Volodymyr
  • 508
  • 1
  • 8
  • 16
12

Similar to some of the above answers but using a basic RegEx is how I solved it instead:

let fileName = parseFilenameFromContentDisposition(response.headers('Content-Disposition'));

function parseFilenameFromContentDisposition(contentDisposition) {
    if (!contentDisposition) return null;
    let matches = /filename="(.*?)"/g.exec(contentDisposition);

    return matches && matches.length > 1 ? matches[1] : null;
}
Pat Migliaccio
  • 1,031
  • 13
  • 24
  • 2
    This is actually superior to the other answers because the other answers depend on the filename being the second token of the disposition header. If it's not for some reason, then they would return the wrong data. – crush Sep 11 '18 at 14:25
  • Slight modification if there may be some other properties after filename: `filename=?([^;]*)`. You may also be aware of " characters then I would suggest `filename=\"?([^;"]*?)` – Łukasz Kotyński Apr 10 '20 at 16:08
6

Use response.headers to get http response headers:

$http.get(myUrl).then(function (response) {
    // extract filename from response.headers('Content-Disposition')
} 
Michel
  • 26,600
  • 6
  • 64
  • 69
  • I already tried this. I should have mentioned it before, so I appended this to my question. – Vivian River Oct 09 '15 at 20:58
  • 4
    Indeed, you will have to expose the header with `Access-Control-Expose-Headers: Content-Disposition`. This assumes you have control of the server side response... – Michel Oct 09 '15 at 21:05
  • I'm having trouble figuring this out. Do I need the "Access-Control-Expose-Headers" on the "preflight" response or the response that contains the actual file? – Vivian River Oct 10 '15 at 00:59
  • This is how you do it on a rails backend: http://jaketrent.com/post/expose-http-headers-in-cors/ – Nuno Silva Jul 18 '16 at 15:56
5

// SERVICE

downloadFile(params: any): Observable<HttpResponse<any>> {

    const url = `https://yoururl....etc`;


    return this.http.post<HttpResponse<any>>(
      url,
      params,
      {
        responseType: 'blob' as 'json',
        observe: 'response' as 'body'
      })
      .pipe(
        catchError(err => throwError(err))
      );
  }

// COMPONENT

import * as FileSaver from 'file-saver';

... some code

  download(param: any) {
    this.service.downloadFile(param).pipe(
    ).subscribe({
      next: (response: any) => {

        let fileName = 'file';
        const contentDisposition = response.headers.get('Content-Disposition');
        if (contentDisposition) {
          const fileNameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
          const matches = fileNameRegex.exec(contentDisposition);
          if (matches != null && matches[1]) {
            fileName = matches[1].replace(/['"]/g, '');
          }
        }

        const fileContent = response.body;

        FileSaver.saveAs(fileContent, fileName);

      },
      error: (error) => {

        console.log({error});

      }
    });
  }

Enjoy

Hector
  • 636
  • 8
  • 16
4

Maybe you already find solution, but I will post this answer if someone else has this problem.

Add these parameters in the success callback function from the $http request:

    $http.get(myUrl).success(function (data, status, headers, config) {
        // extract filename from headers('Content-Disposition')
    });
4

If response.headers('Content-Disposition') returns null, use response.headers.**get**('Content-Disposition');.

The rest of @andrew's snippet now works great.

Eric Aya
  • 69,473
  • 35
  • 181
  • 253
andreisrob
  • 1,615
  • 15
  • 11
1
success(function(data, status, headers, response,xhr) {
    console.log(headers('Content-Disposition'));
}
Bernard
  • 465
  • 4
  • 3
0

There are a lot of other good answers here - here's what I ended up with that worked best for me against an ASP.NET Core 3.1 server, using a lot of these as a guide.

function getFilename() {
    const header = response.headers.get("Content-Disposition");
    if (!header) {
        return null;
    }

    let matches = /filename=\"?([^;"]+)\"?;?/g.exec(header);

    return matches && matches.length > 1 ? matches[1] : null;
}
0

Many answers here and in another thread solve OP's specific case or are even more general. I believe that you should start with parse function from content-disposition npm package. But since I failed to make this package work in my Angular 12 app (even with attempts analogous to this comment), and other answers here don't satisfy my cases, I've created yet another function.

My flag case of a filename is Tyłe;k Mopka.png, that results in a valid response header:

content-disposition: attachment; filename="Ty_ek; Mopka.png"; filename*=UTF-8''Ty%C5%82ek%3B%20Mopka.png

We've got here: a non ISO-8859-1 character, a space, a semicolon. The last one is especially interesting, not only because of parameters splitting, but also because of percent encoding (decodeURI is not enough, we need to unescape it).

Solution

export function parseContentDispositionFilename(contentDisposition: string): string {
  const filename = getFilename(contentDisposition);
  if (filename) {
    return unescape(decodeURI(filename));
  }
  else {
    throw new Error('content-disposition filename cannot be empty');
  }
}

function getFilename(contentDisposition: string): string | undefined {
  const filenames = getFilenameParams(contentDisposition);

  if (filenames.filenamestar) {
    // RFC 6266 4.1 filename* -> RFC 5987 3.2.1 ext-value
    return filenames.filenamestar.replace(/^(?<charset>.+)'(?<language>.*)'(?<filename>.+)$/, '$<filename>');
  }
  else if (filenames.filename) {
    // RFC 6266 4.1 filename (possibly quoted)
    return filenames.filename.replace(/^"(?<filename>.+)"$/, '$<filename>');
  }
  else {
    return undefined;
  }
}

function getFilenameParams(contentDisposition: string): { filenamestar?: string, filename?: string } {
  // Split using ; (if not quoted) and skip the first element since it's `disposition-type`
  const [, ...dispositionParams] = contentDisposition.split(/(?!\B"[^"]*);\s(?![^"]*"\B)/);
  return {
    filenamestar: getParamValue('filename\\*', dispositionParams),
    filename: getParamValue('filename', dispositionParams),
  };
}

function getParamValue(paramName: string, params: string[]): string | undefined {
  const regex = new RegExp('^\\s*' + paramName + '=(?<paramValue>.+)\\s*$', 'i');
  return params.find(p => p.match(regex)?.groups?.['paramValue']);
}

Usage

this.http.get(/*...*/).pipe(
  map(response => {
    const contentDisposition = response.headers.get('content-disposition');
    if (!contentDisposition) {
      throw new Error('content-disposition header not found');
    }

    const filename = parseContentDispositionFilename(contentDisposition);

/*...*/
foka
  • 804
  • 9
  • 27
0

(file gets saved in binary format in the browser. the filename is in client's Network/header/Content-Disposition, we need to fetch the filename)

In Server-side code:
node js code-
    response.setHeader('Access-Control-Expose-Headers','Content-Disposition');
    response.download(outputpath,fileName);   

In client-side code:
1)appComponent.ts file
import { HttpHeaders } from '@angular/common/http';
this.reportscomponentservice.getReportsDownload(this.myArr).subscribe((event: any) => {
 var contentDispositionData= event.headers.get('content-disposition');
 let filename = contentDispositionData.split(";")[1].split("=")[1].split('"')[1].trim()
 saveAs(event.body, filename); 
});

2) service.ts file
import { HttpClient, HttpResponse } from '@angular/common/http';
getReportsDownload(myArr): Observable<HttpResponse<Blob>> {
 console.log('Service Page', myArr);
 return this.http.post(PowerSimEndPoints.GET_DOWNLOAD_DATA.PROD, myArr, {
  observe: 'response',
  responseType: 'blob'
 });
}
swati bohidar
  • 79
  • 1
  • 3