Angular 5 manage http get with blob response and json errors
Asked Answered
O

6

24

I'm working on an Angular 5 application. I have to download a file from my backend-application and to do this I simply invoke a function like this:

public executeDownload(id: string): Observable<Blob> {
  return this.http.get(this.replaceUrl('app/download', denunciaId), {responseType: 'blob'}).map(result => {
    return result;
  });
}

And to invoke the download service I just invoke:

public onDownload() {
  this.downloadService.executeDownload(this.id).subscribe(res => {
    saveAs(res, 'file.pdf');
  }, (error) => {
    console.log('TODO', error);
    // error.error is a Blob but i need to manage it as RemoteError[]
  });
}

When the backend application is in a particular state, instead of returning a Blob, it returns an HttpErrorResponse that contains in its error field an array of RemoteError. RemoteError is an interface that I wrote to manage remote errors.

In catch function, error.error is a Blob. How can I translate Blob attribute into an array of RemoteError[]?

Thanks in advance.

Oceania answered 25/3, 2018 at 19:19 Comment(0)
P
1

As in docs "The only way to read content from a Blob is to use a FileReader." https://developer.mozilla.org/en-US/docs/Web/API/Blob.

EDIT: If you need part of blob, you can do a slice, which returns new Blob, and then use file reader.

Postmark answered 25/3, 2018 at 22:29 Comment(0)
A
22

This is a known Angular issue, and in that thread JaapMosselman provides a very nice solution that involves creating an HttpInterceptor which will translate the Blob back to JSON.

Using this approach, you don't have to do conversions throughout your application, and when the issue is fixed, you can simply remove it.

import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpHandler, HttpRequest, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class BlobErrorHttpInterceptor implements HttpInterceptor {
    public intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        return next.handle(req).pipe(
            catchError(err => {
                if (err instanceof HttpErrorResponse && err.error instanceof Blob && err.error.type === "application/json") {
                    // https://github.com/angular/angular/issues/19888
                    // When request of type Blob, the error is also in Blob instead of object of the json data
                    return new Promise<any>((resolve, reject) => {
                        let reader = new FileReader();
                        reader.onload = (e: Event) => {
                            try {
                                const errmsg = JSON.parse((<any>e.target).result);
                                reject(new HttpErrorResponse({
                                    error: errmsg,
                                    headers: err.headers,
                                    status: err.status,
                                    statusText: err.statusText,
                                    url: err.url
                                }));
                            } catch (e) {
                                reject(err);
                            }
                        };
                        reader.onerror = (e) => {
                            reject(err);
                        };
                        reader.readAsText(err.error);
                    });
                }
                return throwError(err);
            })
        );
    }
}

Declare it in your AppModule or CoreModule:

import { HTTP_INTERCEPTORS } from '@angular/common/http';
...

@NgModule({
    ...
    providers: [
        {
            provide: HTTP_INTERCEPTORS,
            useClass: BlobErrorHttpInterceptor,
            multi: true
        },
    ],
    ...
export class CoreModule { }
Antoinetteanton answered 16/8, 2019 at 19:23 Comment(1)
Should be the accepted answer, perfect, copy/paste.Ataraxia
T
4

The suggestions to use FileReader are not enough for me because they don't work with HttpTestingController (because the blob to json conversion is asynchronous). The karma tests in my case always complete before that promise has been resolved. That means I can't write karma tests that test the unhappy paths using this method. I will suggest a solution that converts the blob to json synchronously.

Service class:

public doGetCall(): void {
    this.httpClient.get('/my-endpoint', {observe: 'body', responseType: 'blob'}).subscribe(
        () => console.log('200 OK'),
        (error: HttpErrorResponse) => {
            const errorJson = JSON.parse(this.blobToString(error.error));
            ...
        });
}

private blobToString(blob): string {
    const url = URL.createObjectURL(blob);
    xmlRequest = new XMLHttpRequest();
    xmlRequest.open('GET', url, false);
    xmlRequest.send();
    URL.revokeObjectURL(url);
    return xmlRequest.responseText;
}

Angular test:

it('test error case', () => {
    const response = new Blob([JSON.stringify({error-msg: 'get call failed'})]);

    myService.doGetCall();

    const req = httpTestingController.expectOne('/my-endpoint');
    expect(req.request.method).toBe('GET');
    req.flush(response, {status: 500, statusText: ''});
    ... // expect statements here
});

The parsed errorJson in the error clause will now contain {error-msg: 'get call failed'}.

Thymelaeaceous answered 3/2, 2022 at 19:21 Comment(0)
B
2

Like probably most people, I wanted my error message synchronously. I dealt with the problem by putting it in an alert box:

(err:any) => { 

    // Because result, including err.error, is a blob,
    // we must use FileReader to display it asynchronously:
    var reader = new FileReader();
    reader.onloadend = function(e) {
      alert("Error:\n" + (<any>e.target).result);
    }
    reader.readAsText(err.error);

    let errorMessage = "Error: " + err.status.toString() + " Error will display in alert box.";
    // your code here to display error messages.
},
Bissau answered 15/7, 2020 at 18:50 Comment(0)
P
1

As in docs "The only way to read content from a Blob is to use a FileReader." https://developer.mozilla.org/en-US/docs/Web/API/Blob.

EDIT: If you need part of blob, you can do a slice, which returns new Blob, and then use file reader.

Postmark answered 25/3, 2018 at 22:29 Comment(0)
C
0

This issue can be resolved by adding 'Content-Type':'application/json' in the header.

Casandracasanova answered 4/8, 2023 at 10:52 Comment(0)
R
-1

The response is expected to be a Blob, but apparently it isn't the case. To avoid this error, change the responseType from blob to arraybuffer.

public executeDownload(id: string): Observable<Blob> {
  return this.http.get(this.replaceUrl('app/download', denunciaId), {responseType: 'arraybuffer'}).map(result => {
    return result;
  });
}
Roast answered 12/6, 2020 at 9:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.