Angular 5 manage http get with blob response and json errors

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 { }

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