81

I need download an excel from my backend, its returned a file.

When I do the request I get the error:

TypeError: You provided 'undefined' where a stream was expected. You can provide an Observable, Promise, Array, or Iterable.

My code is:

this.http.get(`${environment.apiUrl}/...`)
      .subscribe(response => this.downloadFile(response, "application/ms-excel"));

I tried get and map(...) but didn't work.

Details: angular 5.2

references:

import { HttpClient } from '@angular/common/http';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/finally';
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/catch';

Content-Type of response:

Content-Type: application/ms-excel

What's wrong?

A. Morel
  • 9,210
  • 4
  • 56
  • 45
Jean Carlos
  • 1,613
  • 3
  • 18
  • 26

9 Answers9

137

Blobs are returned with file type from backend. The following function will accept any file type and popup download window:

downloadFile(route: string, filename: string = null): void{

    const baseUrl = 'http://myserver/index.php/api';
    const token = 'my JWT';
    const headers = new HttpHeaders().set('authorization','Bearer '+token);
    this.http.get(baseUrl + route,{headers, responseType: 'blob' as 'json'}).subscribe(
        (response: any) =>{
            let dataType = response.type;
            let binaryData = [];
            binaryData.push(response);
            let downloadLink = document.createElement('a');
            downloadLink.href = window.URL.createObjectURL(new Blob(binaryData, {type: dataType}));
            if (filename)
                downloadLink.setAttribute('download', filename);
            document.body.appendChild(downloadLink);
            downloadLink.click();
        }
    )
}
Hasan
  • 1,814
  • 1
  • 12
  • 14
80

Try something like this:

type: application/ms-excel

/**
 *  used to get file from server
 */

this.http.get(`${environment.apiUrl}`,{
          responseType: 'arraybuffer',headers:headers} 
         ).subscribe(response => this.downLoadFile(response, "application/ms-excel"));


    /**
     * Method is use to download file.
     * @param data - Array Buffer data
     * @param type - type of the document.
     */
    downLoadFile(data: any, type: string) {
        let blob = new Blob([data], { type: type});
        let url = window.URL.createObjectURL(blob);
        let pwa = window.open(url);
        if (!pwa || pwa.closed || typeof pwa.closed == 'undefined') {
            alert( 'Please disable your Pop-up blocker and try again.');
        }
    }
Akj
  • 7,038
  • 3
  • 28
  • 40
  • 4
    I got an error using Angular 9: Type '"arraybuffer"' is not assignable to type '"json"'.ts(2322) http.d.ts(1097, 9): The expected type comes from property 'responseType'. – programmer-man Mar 01 '20 at 13:18
  • @programmer-man You now need to do something like: resposeType: 'arrayheaders' as 'json' , see Hasan's answer. – Sean Halls Apr 20 '20 at 14:52
  • 1
    I modify the download file and I can set the filename and works perfect me: ` downLoadFile(data: any, type: string) { const fileName = 'file1.xlsx'; const a = document.createElement('a'); document.body.appendChild(a); a.style = 'display: none'; const blob = new Blob([data], {type: type}); const url = window.URL.createObjectURL(blob); a.href = url; a.download = fileName; a.click(); window.URL.revokeObjectURL(url); } ` – gabrielrincon May 14 '20 at 16:44
27

It took me a while to implement the other responses, as I'm using Angular 8 (tested up to 13). I ended up with the following code (heavily inspired by Hasan).

Note that for the name to be set, the header Access-Control-Expose-Headers MUST include Content-Disposition. To set this in django RF:

http_response = HttpResponse(package, content_type='application/javascript')
http_response['Content-Disposition'] = 'attachment; filename="{}"'.format(filename)
http_response['Access-Control-Expose-Headers'] = "Content-Disposition"

In angular:

  // component.ts
  // getFileName not necessary, you can just set this as a string if you wish
  getFileName(response: HttpResponse<Blob>) {
    let filename: string;
    try {
      const contentDisposition: string = response.headers.get('content-disposition');
      const r = /(?:filename=")(.+)(?:;")/
      filename = r.exec(contentDisposition)[1];
    }
    catch (e) {
      filename = 'myfile.txt'
    }
    return filename
  }

  
  downloadFile() {
    this._fileService.downloadFile(this.file.uuid)
      .subscribe(
        (response: HttpResponse<Blob>) => {
          let filename: string = this.getFileName(response)
          let binaryData = [];
          binaryData.push(response.body);
          let downloadLink = document.createElement('a');
          downloadLink.href = window.URL.createObjectURL(new Blob(binaryData, { type: 'blob' }));
          downloadLink.setAttribute('download', filename);
          document.body.appendChild(downloadLink);
          downloadLink.click();
        }
      )
  }

  // service.ts
  downloadFile(uuid: string) {
    return this._http.get<Blob>(`${environment.apiUrl}/api/v1/file/${uuid}/package/`, { observe: 'response', responseType: 'blob' as 'json' })
  }

Preston
  • 7,399
  • 8
  • 54
  • 84
  • Yes, it works and I've been using it for a few weeks now, but I came across the fact, that it only works up to a certain filesize. If the returned data is too large (like 1 - 2 MB), the download window doesn't show up. Plus: Even if it worked, on real big files, you wouldn't see the save dialog until all the data has been received. Not a real download... – Satria Aug 17 '20 at 09:26
  • 3
    thanks, it worked except for the regex, I'm using asp.net and so the content disposition was a bit different, mine looks like this: '/(?:filename=)(.+)(?:;)/' – Eluvatar Dec 04 '20 at 00:00
  • 1
    Thanks! Can confirm this also works with Angular 13 – Matt List May 17 '22 at 09:22
  • Like this answer as it uses a file name sent from the server, however, the regex fails when the file name has space(s) so I tried fixing the regex to `(?:filename="?)([^"]+)(?:("?;))`. Have a quick test at [regex101](https://regex101.com/r/as9EAF/1) – Mihir Aug 12 '22 at 15:06
9

Using Blob out put from API (Excel FIle)

and tweaked @gabrielrincon answer

downloadExcel(): void {
const payload = {
  order: 'test',
  };

this.service.downloadExcel(payload)
  .subscribe((res: any) => {
    this.blobToFile(res, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Export.xlsx");
  });}

blob to file common function

blobToFile(data: any, type: string, fileName: string) {
 const a = document.createElement('a');
 document.body.appendChild(a);
 a.style.display = 'none';
 const blob = new Blob([data], { type: type });
 const url = window.URL.createObjectURL(blob);
 a.href = url; a.download = fileName; a.click();
 window.URL.revokeObjectURL(url);}

in the blob to file function we are expecting first parameter as our blob data, type of file and pass file name including extention 1. we are creating an html a tag element 2. then we append the element in html 3. then hide the a tag element 4. then create new blob object with file and type 5. we will convert the blob object to URL 6. then appened that URL to href property of our a tag 7. we are opening our URL in window so it will download

Shabeer M
  • 160
  • 3
  • 9
  • Can you please explain the blobToFile function? – cNgamba Jul 22 '21 at 20:39
  • in the blob to file function we are expecting first parameter as our blob data, type of file and pass file name including extention 1. we are creating an html a tag element 2. then we append the element in html 3. then hide the a tag element 4. then create new blob object with file and type 5. we will convert the blob object to URL 6. then appened that URL to href property of our a tag 7. we are opening our URL in window so it will download – Shabeer M Jul 26 '21 at 07:51
  • Thanks. Btw how can I display a message on any error? – cNgamba Jul 28 '21 at 14:12
  • Say for example if the file is not found I wanted to display a message in my UI – cNgamba Jul 28 '21 at 14:21
5

I ended up here when searching for ”rxjs download file using post”.

This was my final product. It uses the file name and type given in the server response.

import { ajax, AjaxResponse } from 'rxjs/ajax';
import { map } from 'rxjs/operators';

downloadPost(url: string, data: any) {
    return ajax({
        url: url,
        method: 'POST',
        responseType: 'blob',
        body: data,
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'text/plain, */*',
            'Cache-Control': 'no-cache',
        }
    }).pipe(
        map(handleDownloadSuccess),
    );
}


handleDownloadSuccess(response: AjaxResponse) {
    const downloadLink = document.createElement('a');
    downloadLink.href = window.URL.createObjectURL(response.response);

    const disposition = response.xhr.getResponseHeader('Content-Disposition');
    if (disposition) {
        const filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
        const matches = filenameRegex.exec(disposition);
        if (matches != null && matches[1]) {
            const filename = matches[1].replace(/['"]/g, '');
            downloadLink.setAttribute('download', filename);
        }
    }

    document.body.appendChild(downloadLink);
    downloadLink.click();
    document.body.removeChild(downloadLink);
}
Mikael M
  • 1,375
  • 14
  • 14
4

After spending much time searching for a response to this answer: how to download a simple image from my API restful server written in Node.js into an Angular component app, I finally found a beautiful answer in this web Angular HttpClient Blob. Essentially it consist on:

API Node.js restful:

   /* After routing the path you want ..*/
  public getImage( req: Request, res: Response) {

    // Check if file exist...
    if (!req.params.file) {
      return res.status(httpStatus.badRequest).json({
        ok: false,
        msg: 'File param not found.'
      })
    }
    const absfile = path.join(STORE_ROOT_DIR,IMAGES_DIR, req.params.file);

    if (!fs.existsSync(absfile)) {
      return res.status(httpStatus.badRequest).json({
        ok: false,
        msg: 'File name not found on server.'
      })
    }
    res.sendFile(path.resolve(absfile));
  }

Angular 6 tested component service (EmployeeService on my case):

  downloadPhoto( name: string) : Observable<Blob> {
    const url = environment.api_url + '/storer/employee/image/' + name;

    return this.http.get(url, { responseType: 'blob' })
      .pipe(
        takeWhile( () => this.alive),
        filter ( image => !!image));
  }

Template

 <img [src]="" class="custom-photo" #photo>

Component subscriber and use:

@ViewChild('photo') image: ElementRef;

public LoadPhoto( name: string) {
    this._employeeService.downloadPhoto(name)
          .subscribe( image => {
            const url= window.URL.createObjectURL(image);
            this.image.nativeElement.src= url;
          }, error => {
            console.log('error downloading: ', error);
          })    
}
double-beep
  • 5,031
  • 17
  • 33
  • 41
Marc
  • 289
  • 2
  • 5
3

May be I am late. But last answer by @Hasan was superb.

I just make little changes(this was not accepting headers so removed) in this and got success.

downloadFile(route: string, filename: string = null): void {
    // const baseUrl = 'http://myserver/index.php/api';   
    this.http.get(route, { responseType: 'blob' }).subscribe(
      (response: any) => {
        let dataType = response.type;
        let binaryData = [];
        binaryData.push(response);
        let downloadLink = document.createElement('a');
        downloadLink.href = window.URL.createObjectURL(new Blob(binaryData, { type: dataType }));
        if (filename) {
          downloadLink.setAttribute('download', filename);
        }
        document.body.appendChild(downloadLink);
        downloadLink.click();
      }
    )
  }
Deepak Dholiyan
  • 1,774
  • 1
  • 20
  • 35
2

Using Blob as a source for an img:

template:

<img [src]="url">

component:

 public url : SafeResourceUrl;

 constructor(private http: HttpClient, private sanitizer: DomSanitizer) {
   this.getImage('/api/image.jpg').subscribe(x => this.url = x)
 }

 public getImage(url: string): Observable<SafeResourceUrl> {
   return this.http
     .get(url, { responseType: 'blob' })
     .pipe(
       map(x => {
         const urlToBlob = window.URL.createObjectURL(x) // get a URL for the blob
         return this.sanitizer.bypassSecurityTrustResourceUrl(urlToBlob); // tell Anuglar to trust this value
       }),
     );
 }

Further reference about trusting save values

Rafi Henig
  • 5,950
  • 2
  • 16
  • 36
  • I needed those bits about `SafeResourceUrl` and `bypassSecurityTrustResourceUrl` to resolve problems I was having. – Zarepheth Dec 08 '21 at 19:19
1

Keep it simple, i am stunned by all the complicated proposed solutions.

If your backend returns the attachment flag and the filename property in its Content-Disposition, you can simply make this javascript call.

window.location = `${environment.apiUrl}/...`;

The browser will download the file without changing the current page.

Matthieu Charbonnier
  • 2,794
  • 25
  • 33