78

I have my REST API where I put my pdf file, now I want my angular app to download it on click via my web browser but I got HttpErrorResponse

"Unexpected token % in JSON at position 0"

"SyntaxError: Unexpected token % in JSON at position 0↵ at JSON.parse (

this is my endpoint

    @GetMapping("/help/pdf2")
public ResponseEntity<InputStreamResource> getPdf2(){

    Resource resource = new ClassPathResource("/pdf-sample.pdf");
    long r = 0;
    InputStream is=null;

    try {
        is = resource.getInputStream();
        r = resource.contentLength();
    } catch (IOException e) {
        e.printStackTrace();
    }

        return ResponseEntity.ok().contentLength(r)
                .contentType(MediaType.parseMediaType("application/pdf"))
                .body(new InputStreamResource(is));

}

this is my service

  getPdf() {

this.authKey = localStorage.getItem('jwt_token');

const httpOptions = {
  headers: new HttpHeaders({
    'Content-Type':  'application/pdf',
    'Authorization' : this.authKey,
    responseType : 'blob',
    Accept : 'application/pdf',
    observe : 'response'
  })
};
return this.http
  .get("http://localhost:9989/api/download/help/pdf2", httpOptions);

}

and invocation

this.downloadService.getPdf()
  .subscribe((resultBlob: Blob) => {
  var downloadURL = URL.createObjectURL(resultBlob);
  window.open(downloadURL);});
gmexo
  • 1,892
  • 2
  • 14
  • 19

7 Answers7

106

I resolved it as follows:

// header.component.ts
this.downloadService.getPdf().subscribe((data) => {

  this.blob = new Blob([data], {type: 'application/pdf'});

  var downloadURL = window.URL.createObjectURL(data);
  var link = document.createElement('a');
  link.href = downloadURL;
  link.download = "help.pdf";
  link.click();

});



//download.service.ts
getPdf() {

  const httpOptions = {
    responseType: 'blob' as 'json')
  };

  return this.http.get(`${this.BASE_URL}/help/pdf`, httpOptions);
}
gmexo
  • 1,892
  • 2
  • 14
  • 19
  • 6
    I hate to post the simple thank you comments as they are usually not value added...however I had to for this one. I had been searching struggling with this for WAY too many hours today. I had been searching for the code you have from ```this.blob``` down to ```link.click()``` you just saved me what I thought was going to be an all nighter! Thanks!!!!!! – bemon Feb 07 '19 at 02:08
  • 1
    Thanks, works without needing any plugins etc. Saved my day, big thumbs up for you. – skydev May 29 '19 at 04:31
  • 1
    Hi ... I did the same but I think angular routing is handling that link and then in the file I will get my home page(which angular will load if it is not find any thing ) content in the file not the content I have sent from the api. Can anyone help me with this ? – Meysam Jul 13 '19 at 09:33
  • 1
    Thanks working cool , just need to remove line "this.blob = new Blob([data], {type: 'application/pdf'});" – Bhagvat Lande Aug 22 '19 at 07:38
  • 3
    window.URL.createObjectURL is deprecated. this is wrong. – tatsu Nov 18 '19 at 15:39
  • 2
    You should pass this.blob to createObjectURL, not data. var downloadURL = window.URL.createObjectURL(this.blob); – Ε Г И І И О Nov 10 '20 at 13:12
  • 3
    use URL.createObjectURL, not window.URL.createObjectURL, apparently newer browsers consider the window.URL.createObjectURL to be a security risk – Christopher Thomas Dec 29 '20 at 22:46
  • Thank a lot. you save my time. – Shehan Silva May 16 '21 at 14:58
  • Thanks, `responseType: 'blob'` solved my problem – Ahmed Wahba Aug 03 '21 at 08:19
  • thanks, good a simple idea, very good job. A detail, aleternativa if ou want open the file in other tab. You change ´link.download = "help.pdf"´ for this ´link.setAttribute('target', '_blank')´ – Cristian Budzicz Feb 15 '22 at 00:55
92

I solved the issue in this way (please note that I have merged multiple solutions found on stack overflow, but I cannot find the references. Feel free to add them in the comments).

In My service I have:

public getPDF(): Observable<Blob> {   
//const options = { responseType: 'blob' }; there is no use of this
    let uri = '/my/uri';
    // this.http refers to HttpClient. Note here that you cannot use the generic get<Blob> as it does not compile: instead you "choose" the appropriate API in this way.
    return this.http.get(uri, { responseType: 'blob' });
}

In the component, I have (this is the part merged from multiple answers):

public showPDF(fileName: string): void {
    this.myService.getPDF()
        .subscribe(x => {
            // It is necessary to create a new blob object with mime-type explicitly set
            // otherwise only Chrome works like it should
            var newBlob = new Blob([x], { type: "application/pdf" });
            
            // IE doesn't allow using a blob object directly as link href
            // instead it is necessary to use msSaveOrOpenBlob
            if (window.navigator && window.navigator.msSaveOrOpenBlob) {
                window.navigator.msSaveOrOpenBlob(newBlob, fileName);
                return;
            }
            
            // For other browsers: 
            // Create a link pointing to the ObjectURL containing the blob.
            const data = window.URL.createObjectURL(newBlob);
            
            var link = document.createElement('a');
            link.href = data;
            link.download = fileName;
            // this is necessary as link.click() does not work on the latest firefox
            link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
            
            setTimeout(function () {
                // For Firefox it is necessary to delay revoking the ObjectURL
                window.URL.revokeObjectURL(data);
                link.remove();
            }, 100);
        });
}

The code above works in IE, Edge, Chrome and Firefox. However, I don't really like it, as my component is pulluted with browser specific stuff which will surely change over time.

J Scott
  • 781
  • 9
  • 12
Yennefer
  • 5,704
  • 7
  • 31
  • 44
  • working like charm and I commented "const options = { responseType: 'blob' };" this line because of no use. – Sachin from Pune Oct 18 '18 at 12:18
  • Thank you!:) it was a leftover of my original code. – Yennefer Oct 18 '18 at 15:24
  • Don't you have to somehow remove the link from DOM? – Luke1988 Dec 20 '18 at 12:08
  • As a rule of thumb, you should. I couldn't find evidences of leaks. However, I edited the post in order to add the element removal. Thank you for pointing it out. – Yennefer Dec 20 '18 at 13:06
  • @Pier Could you pls open github repo for this functionality. am trying to achieve the same workaround with Node.Js/Express and Angular. am facing a similar issue but not able to sort it out. i left that development a couple of months ago. after came across your solution i plan to retake the challenge. could you pls help me? – Mr. Learner Feb 21 '19 at 06:56
  • It works well however you should think to create a reusable pdf.utils.ts file with the function inside! – pegaltier May 06 '19 at 14:35
  • 1
    window.URL.createObjectURL is deprecated and has been for awhile this answer is outdated and no longer works. – tatsu Nov 18 '19 at 15:39
  • 2
    this answer still works for Chrome: 78.0.3904.97 and Firefox: 70.0.1. However, I cannot find a reliable way to work around that. How would you fix it? – Yennefer Nov 18 '19 at 16:50
  • How do you handle errors coming from the server? http 400 e.g – USQ91 Mar 05 '21 at 05:56
  • @USQ91 I handle those errors in two different places *albeit ina minimal way": Response errors are handled by the observable that is returned by myservice.GetPDFm and in case I also have a couple of interceptors in order to retry the operation. In-transit errors are handled by the SPA, and currently, just a toast is shown – Yennefer Mar 05 '21 at 07:01
  • Thanks, @Yennefer. I'm using a similar solution. But the errors from the server are not showing up in the err part of the observable. It returns a blob and not a JSON of errors. And when I inspect the network req in the browser, I do see the errors there as a JSON object in the Response. Not sure if the "responseType: Blob" is the reason for that. Keen to see how you are handling it. – USQ91 Mar 08 '21 at 02:16
9

For Angular 12+, I came up with something like this:

this.ApiService
    .getFileFromApi()
    .pipe(take(1))
    .subscribe((response) => {
        const downloadLink = document.createElement('a');
        downloadLink.href = URL.createObjectURL(new Blob([response.body], { type: response.body.type }));

        const contentDisposition = response.headers.get('content-disposition');
        const fileName = contentDisposition.split(';')[1].split('filename')[1].split('=')[1].trim();
        downloadLink.download = fileName;
        downloadLink.click();
    });

The subscribe is on a simple get() with the Angular HttpClient.

// api-service.ts

getFileFromApi(url: string): Observable<HttpResponse<Blob>> {
  return this.httpClient.get<Blob>(this.baseApiUrl + url, { observe: 'response', responseType: 'blob' as 'json'});
}
Willem de Vries
  • 131
  • 1
  • 4
3

You can do it with angular directives:

@Directive({
    selector: '[downloadInvoice]',
    exportAs: 'downloadInvoice',
})
export class DownloadInvoiceDirective implements OnDestroy {
    @Input() orderNumber: string;
    private destroy$: Subject<void> = new Subject<void>();
    _loading = false;

    constructor(private ref: ElementRef, private api: Api) {}

    @HostListener('click')
    onClick(): void {
        this._loading = true;
        this.api.downloadInvoice(this.orderNumber)
            .pipe(
                takeUntil(this.destroy$),
                map(response => new Blob([response], { type: 'application/pdf' })),
            )
            .subscribe((pdf: Blob) => {
                this.ref.nativeElement.href = window.URL.createObjectURL(pdf);
                this.ref.nativeElement.click();
            });
    }
    
    // your loading custom class
    @HostBinding('class.btn-loading') get loading() {
        return this._loading;
    }

    ngOnDestroy(): void {
        this.destroy$.next();
        this.destroy$.complete();
    }
}

In the template:

<a
      downloadInvoice
      [orderNumber]="order.number"
      class="btn-show-invoice"
  >
     Show invoice
  </a>
Andreo
  • 41
  • 5
  • This approach is neater and i love it. However there is one issue with it. When you call click on the host element in the subscription, you are triggering the onClick function again and this cycle continues in an infinite loop. You might want to check that. – adedayojs Feb 23 '23 at 01:10
0

My answer is based on @Yennefer's, but I wanted to use the file name from the server since I didn't have it in my FE. I used the Content-Disposition header to transmit this, since that is what the browser uses for a direct download.

First, I needed access to the headers from the request (notice the get method options object):

public getFile(): Observable<HttpResponse<Blob>> {   
    let uri = '/my/uri';
    return this.http.get(uri, { responseType: 'blob', observe: 'response' });
}

Next, I needed to extract the file name from the header.

public getFileName(res: HttpResponse<any>): string {
    const disposition = res.headers.get('Content-Disposition');
    if (!disposition) {
        // either the disposition was not sent, or is not accessible
        //  (see CORS Access-Control-Expose-Headers)
        return null;
    }
    const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:; |$)/;
    const asciiFilenameRegex = /filename=(["'])(.*?[^\\])\1(?:; |$)/;

    let fileName: string = null;
    if (utf8FilenameRegex.test(disposition)) {
      fileName = decodeURIComponent(utf8FilenameRegex.exec(disposition)[1]);
    } else {
      const matches = asciiFilenameRegex.exec(disposition);
      if (matches != null && matches[2]) {
        fileName = matches[2];
      }
    }
    return fileName;
}

This method checks for both ascii and utf-8 encoded file names, prefering utf-8.

Once I have the file name, I can update the download property of the link object (in @Yennifer's answer, that's the lines link.download = 'FileName.ext' and window.navigator.msSaveOrOpenBlob(newBlob, 'FileName.ext');)

A couple of notes on this code:

  1. Content-Disposition is not in the default CORS whitelist, so it may not be accessible from the response object based on the your server's configuration. If this is the case, in the response server, set the header Access-Control-Expose-Headers to include Content-Disposition.

  2. Some browsers will further clean up file names. My version of chrome seems to replace : and " with underscores. I'm sure there are others but that's out of scope.

J Scott
  • 781
  • 9
  • 12
0
//Step: 1
//Base Service
this.getPDF() {
 return this.http.get(environment.baseUrl + apiUrl, {
      responseType: 'blob',
      headers: new HttpHeaders({
        'Access-Control-Allow-Origin': '*',
        'Authorization': localStorage.getItem('AccessToken') || ''
      })
    });
}

//Step: 2
//downloadService
getReceipt() {
    return new Promise((resolve, reject) => {
      try {
        // {
        const apiName = 'js/getReceipt/type/10/id/2';
        this.getPDF(apiName).subscribe((data) => {
          if (data !== null && data !== undefined) {
            resolve(data);
          } else {
            reject();
          }
        }, (error) => {
          console.log('ERROR STATUS', error.status);
          reject(error);
        });
      } catch (error) {
        reject(error);
      }
    });
  }


//Step 3:
//Component 
getReceipt().subscribe((respect: any) => {
  var downloadURL = window.URL.createObjectURL(data);
  var link = document.createElement(‘a’);
  link.href = downloadURL;
  link.download = “sample.pdf";
  link.click();
});
Siddharth
  • 31
  • 1
-1

This also works in IE and Chrome, almost the same answer only for other browsers the answer is a bit shorter.

getPdf(url: string): void {
    this.invoiceService.getPdf(url).subscribe(response => {

      // It is necessary to create a new blob object with mime-type explicitly set
      // otherwise only Chrome works like it should
      const newBlob = new Blob([(response)], { type: 'application/pdf' });

      // IE doesn't allow using a blob object directly as link href
      // instead it is necessary to use msSaveOrOpenBlob
      if (window.navigator && window.navigator.msSaveOrOpenBlob) {
          window.navigator.msSaveOrOpenBlob(newBlob);
          return;
      }

      // For other browsers:
      // Create a link pointing to the ObjectURL containing the blob.
      const downloadURL = URL.createObjectURL(newBlob);
        window.open(downloadURL);
    });
  } 
Aswathy
  • 179
  • 7