Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 5 manage http get with blob response and json errors

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.

like image 919
xcesco Avatar asked Mar 25 '18 19:03

xcesco


4 Answers

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 { }
like image 81
Marcos Dimitrio Avatar answered Nov 11 '22 05:11

Marcos Dimitrio


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.
},
like image 20
Bruce Patin Avatar answered Nov 11 '22 06:11

Bruce Patin


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'}.

like image 3
Babyburger Avatar answered Nov 11 '22 06:11

Babyburger


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.

like image 1
Vayrex Avatar answered Nov 11 '22 06:11

Vayrex