How to Retrieve Files Using Angular 6 from a RESTful API

A challenge I am facing is with my REST API where I have uploaded a PDF file. I am now trying to enable my Angular app to download the file when a user clicks on a button through the web browser. However, I am encountering an HttpErrorResponse with the error message:

"Unexpected token % in JSON at position 0"

After investigating, I found that the issue arises at position 0 due to a syntax error when parsing JSON data. Here is a snippet of my endpoint code:

@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 how I am handling the service in my Angular app:

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);

Finally, here is how I invoke the service to download the PDF file:

this.downloadService.getPdf()
  .subscribe((resultBlob: Blob) => {
  var downloadURL = URL.createObjectURL(resultBlob);
  window.open(downloadURL);});

Answer №1

Here is how I resolved the issue:

// 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);
}

Answer №2

I came up with this solution after consolidating various snippets from stack overflow (although I can't recall the specific sources. Feel free to provide them in the comments).

Within My service:

public fetchPDF(): Observable<Blob> {   
//const options = { responseType: 'blob' }; not needed
    let uri = '/my/uri';
    // Note that using get<Blob> directly won't compile, so the appropriate API selection is done this way
    return this.http.get(uri, { responseType: 'blob' });
}

And in the component (aggregated from multiple solutions):

public displayPDF(fileName: string): void {
    this.myService.fetchPDF()
        .subscribe(x => {
            // Creating a new blob object with explicit mime-type setting is crucial
            // for correct functionality across different browsers
            var newBlob = new Blob([x], { type: "application/pdf" });
            
            // IE requires the use of msSaveOrOpenBlob instead of directly setting blob as link href
            if (window.navigator && window.navigator.msSaveOrOpenBlob) {
                window.navigator.msSaveOrOpenBlob(newBlob, fileName);
                return;
            }
            
            // For other browsers: 
            // Creating 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;
            // Delaying the revoking of ObjectURL for Firefox
            link.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window }));
            
            setTimeout(function () {
                window.URL.revokeObjectURL(data);
                link.remove();
            }, 100);
        });
}

This code handles PDF display in IE, Edge, Chrome, and Firefox. However, I'm not entirely satisfied with the browser-specific additions cluttering my component.

Answer №3

Here's my approach for handling file downloads in Angular versions 12 and above:

this.DownloadService
    .fetchFileFromApi()
    .pipe(take(1))
    .subscribe((response) => {
        const downloadLink = document.createElement('a');
        downloadLink.href = URL.createObjectURL(new Blob([response.content], { type: response.content.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 subscription is part of a simple get() call using the Angular HttpClient.

// download-service.ts

fetchFileFromApi(url: string): Observable<HttpResponse<Blob>> {
  return this.httpClient.get<Blob>(this.baseDownloadUrl + url, { observe: 'response', responseType: 'blob' as 'json'});
}

Answer №4

One approach is to utilize Angular directives for this task:

@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();
            });
    }
    
    // Add your custom loading class
    @HostBinding('class.btn-loading') get loading() {
        return this._loading;
    }

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

Implement the following code in your template:

<a
      downloadInvoice
      [orderNumber]="order.number"
      class="btn-show-invoice"
  >
     Show invoice
  </a>

Answer №5

If you want to prompt the browser to download the file, you can achieve this by adjusting the

Content-Type: 'application/octet-stream;
setting in your server-side code.

Alternatively, if this server-side configuration is not feasible or you prefer to handle the download on the client-side, you can utilize the file-saver library. FileSaver.js simplifies the process significantly.

this.downloadService.getPdf().subscribe((resultBlob: Blob) => {
   saveAs(resultBlob, 'help.pdf', { autoBom: false });
});

Answer №6

While building upon @Yennefer's solution, I took a different approach by extracting the file name from the server since I didn't have it in my front end. I made use of the Content-Disposition header for transmitting this information, as it is commonly used by browsers for direct downloads.

To begin, I required access to the request headers, as shown in the following method with the get options object:

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

Next, I needed a function to extract the file name from the response 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 function handles both ascii and utf-8 encoded file names, with a preference for utf-8.

Once the file name is obtained, I can update the download property of the link object, as demonstrated in @Yennefer's solution (specifically the lines link.download = 'FileName.ext' and

window.navigator.msSaveOrOpenBlob(newBlob, 'FileName.ext');
)

A few key points to note about this code:

  1. The Content-Disposition header is not included in the default CORS whitelist, which may result in it being inaccessible from the response object based on your server's configuration. To address this, ensure that the header Access-Control-Expose-Headers includes Content-Disposition in the response.

  2. Some browsers may modify file names further. For example, in my version of Chrome, colons (:) and quotation marks (") were replaced with underscores. There may be similar alterations in other browsers, but that is beyond the scope of this discussion.

Answer №7

//Procedure: 1
//Primary Function
retrievePDF() {
  return this.http.get(environment.baseUrl + apiUrl, {
      responseType: 'blob',
      headers: new HttpHeaders({
        'Access-Control-Allow-Origin': '*',
        'Authorization': localStorage.getItem('AccessToken') || ''
      })
    });
}

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


//Procedure: 3
//Section 
fetchReceipt().subscribe((result: any) => {
  var downloadURL = window.URL.createObjectURL(data);
  var link = document.createElement(‘a’);
  link.href = downloadURL;
  link.download = “sample.pdf";
  link.click();
});

Answer №8

This method is compatible with Internet Explorer and Chrome, with a slight variation for other web browsers where the code is more concise.

fetchPdfData(url: string): void {
    this.pdfService.fetchPdfData(url).subscribe(response => {

      // To ensure compatibility with Chrome, create a new blob object with the specified mime-type
      const newBlob = new Blob([(response)], { type: 'application/pdf' });

      // For Internet Explorer, cannot directly use blob object as link href, must use msSaveOrOpenBlob
      if (window.navigator && window.navigator.msSaveOrOpenBlob) {
          window.navigator.msSaveOrOpenBlob(newBlob);
          return;
      }

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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can I target the initial last element within an *ngFor loop?

In my current project using Ionic, I am developing a personal application where I aim to implement an alphabetical header/divider for a list of games. The idea is to check the first letter of each game, and whenever it differs from the last game's ini ...

Tips on streamlining two similar TypeScript interfaces with distinct key names

Presented here are two different formats for the same interface: a JSON format with keys separated by low dash, and a JavaScript camelCase format: JSON format: interface MyJsonInterface { key_one: string; key_two: number; } interface MyInterface { ...

Employing distinct techniques for a union-typed variable in TypeScript

I'm currently in the process of converting a JavaScript library to TypeScript. One issue I've encountered is with a variable that can be either a boolean or an array. This variable cannot be separated into two different variables because it&apos ...

HTML template failing to retrieve data from Angular dataSource

My goal is to import data from an Excel file into my angular application. I have successfully retrieved the data from the Excel file, parsed it to extract the necessary columns, and stored it in an array within my service.ts file. I call the service from ...

How to detach functions in JavaScript while preserving their context?

Can functions in JavaScript be detached while still retaining access to their context? For instance, let's say we have an instance of ViewportScroller called vc. We can retrieve the current scroll position with the following method: vc.getScrollPosi ...

Optimal method for organizing REST URIs

Let's consider the scenario where we have the following classes and need to establish REST URIs: class Profile{ IList<Upload> Uploads{get;} } class Upload{ int ProfileId; int CategoryId; } class Category{ IList<Upload> Uploads{ ...

Navigating with Angular 2: Redirecting Azure AD login URI

I am currently exploring Azure AD integration for logging into an Angular 2 application. The Github link provided below demonstrates a simple method to log in without the use of any authentication libraries. Sign in via Microsoft Graph using Typescript an ...

What is the correct way to implement Axios interceptor in TypeScript?

I have implemented an axios interceptor: instance.interceptors.response.use(async (response) => { return response.data; }, (err) => { return Promise.reject(err); }); This interceptor retrieves the data property from the response. The re ...

Unable to initiate ngModelChange event during deep cloning of value

I've been struggling to calculate the sum of row values, with no success. My suspicion is that the issue lies in how I am deep cloning the row values array when creating the row. const gblRowVal1 = new GridRowValues(1, this.color, this.headList ...

Obtaining the accurate return type based on the generic parameter in a generic function

Is there a way to determine the correct return type of a function that depends on a generic argument? function f1<T>(o: T) { return { a: o } } // How can we set T to number through (n: number)? type T1 = typeof f1 extends (n: number) => infe ...

Having issues with Angular material autocomplete feature - not functioning as expected, and no error

I have set up my autocomplete feature, and there are no error messages. However, when I type something in the input field, nothing happens - it seems like there is no action being triggered, and nothing appears in the console. Here is the HTML code: ...

"Discovering the secrets of incorporating a spinner into Angular2 and mastering the art of concealing spinners in Angular

When experiencing delay in image loading time, I need to display a spinner until the image loads completely. What is the best way to achieve this on the Angular 2 platform? <div id='panId' class="container-fluid" > This section ...

Tips for creating React/MobX components that can be reused

After seeing tightly coupled examples of integrating React components with MobX stores, I am seeking a more reusable approach. Understanding the "right" way to achieve this would be greatly appreciated. To illustrate my goal and the challenge I'm fac ...

The TypeScript compiler is indicating that the Observable HttpEvent cannot be assigned to the type Observable

Utilizing REST API in my angular application requires me to create a service class in typescript. The goal is to dynamically switch between different url endpoints and pass specific headers based on the selected environment. For instance: if the environmen ...

Using Angular: Binding Angular variables to HTML for display

I have a component with a ts file that contains HTML content as a variable. Let's say para1= <a href="www.google.com">sitename</a> more content I want to bind this paragraph in HTML as if it were actual HTML tags. sitename What is the ...

Interacting with User Input in React and TypeScript: Utilizing onKeyDown and onChange

Trying to assign both an onChange and onKeyDown event handler to an Input component with TypeScript has become overwhelming. This is the current structure of the Input component: import React, { ChangeEvent, KeyboardEvent } from 'react' import ...

Tips for troubleshooting TypeScript Express application in Visual Studio Code

Recently, I attempted to troubleshoot the TypeScript Express App located at https://github.com/schul-cloud/node-notification-service/ using Visual Studio Code. Within the launch.json file, I included the following configuration: { "name": "notifi ...

Ways to create a versatile function for generating TypedArrays instances

I'm working on a function that looks like this: export function createTypedArray<T extends TypedArray>( arg : { source : T, arraySize : number } ) : T { if( arg.source instanceof Int32Array ) { return new Int32Array( arg.arraySize ); } ...

What is the correct way to utilize Global Variables in programming?

Having trouble incrementing the current page in my pagination script to call the next page via AJAX... In my TypeScript file, I declare a global variable like this; declare var getCurrentPage: number; Later in the same file, I set the value for getCurren ...

Issue with Ionic 4 IOS deeplinks: Instead of opening in the app, they redirect to the browser

After working diligently to establish deeplinks for my Ionic 4 iOS application, I meticulously followed a series of steps to achieve this goal: I uploaded an Apple site association file to the web version of the app, ensuring the utilization of the prec ...