vertika
vertika

Reputation: 1424

How to handle error for response Type blob in HttpRequest

I am calling an http request using httpClient and using response Type as 'blob' but the problem is when it goes in error block the response type remains 'blob'.This is causing problem with my error handling.

this.httpClient.get('http://m502126:3000/reports/perdate', {
observe: 'body',
responseType: 'blob',
params: new HttpParams().set('START_DATE', startDate)
 .set('END_DATE', endDate)
 .set('MIXER', mixer)
 .set('ATTACH', 'true')

 }).subscribe(data => {
 console.log(data);
},
error => {
 console.log(error);

}

)

the problem is i am setting request type as'blob' and type of error is any . So when error comes and goes in error block the response type remains 'blob'. How to handle this ?

Upvotes: 46

Views: 55130

Answers (13)

shivam agrawal
shivam agrawal

Reputation: 1

It can be done by converting blob as text and then parse the text in JSON.

readBlobAsText(blob) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => {
            resolve(reader.result);`enter code here`
        };`enter code here`
        reader.onerror = reject;
        reader.readAsText(blob);
    });
}

this.readBlobAsText(err.error).then((errorRes:any)=> {
                let errObj = JSON.parse(errorRes as string);
                let errorMsg = _.get(errObj,'message');
               console.log(errorMsg)
            })
        });

Upvotes: 0

Marcos Dimitrio
Marcos Dimitrio

Reputation: 6862

The answer by SplitterAlex mentions the Angular issue but doesn't mention a very nice solution provided there by JaapMosselman that involves creating an HttpInterceptor that will translate the Blob back to JSON.

This way, you don't have to implement this 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 } 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 || undefined
                                }));
                            } catch (e) {
                                reject(err);
                            }
                        };
                        reader.onerror = (e) => {
                            reject(err);
                        };
                        reader.readAsText(err.error);
                    });
                }
                throw 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 { }

Upvotes: 25

Ekrem Solmaz
Ekrem Solmaz

Reputation: 707

I had some kind of problems with all the other methods. So here is my solution:

errorHandler(error: HttpErrorResponse) {
    if (error.error instanceof Blob) {
        this.handleBlobError(error);
    }
    // Your default error handling
    // ...
}

handleBlobError(error: HttpErrorResponse) {
    error.error.text().then((res) => {
        const blobError = JSON.parse(res);
        console.warn(blobError.message);
    });
}

Upvotes: 2

Babyburger
Babyburger

Reputation: 1828

My problem with the other answers so far is that 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'}.

Upvotes: 1

Amr Zaghloul
Amr Zaghloul

Reputation: 101

we can solve this problem by two approaches

Solution #1

the first solution is the simplest, is to use the Text() function on the blob object, like this

 this.httpClient.get('http://m502126:3000/reports/perdate', {
    observe: 'body',
    responseType: 'blob',
    params: new HttpParams().set('START_DATE', startDate)
     .set('END_DATE', endDate)
     .set('MIXER', mixer)
     .set('ATTACH', 'true')
    
     }).subscribe(data => {
     console.log(data);
    },
    async error => { // don't forget to use the async
     const message = JSON.parse(await err.error.text())
    }

Solution #2

the second solution is to use a utility function to parse the blob and resolve it with the JSON returned from the blob

private handleBlobError(err: HttpErrorResponse): any {
    return new Promise<string>((resolve, reject) => {
        if ('application/json' === err.headers.get('Content-Type')) {
            const reader = new FileReader();
            reader.addEventListener('loadend', (e) => {
                resolve(JSON.parse(e.srcElement['result']))
            });
            reader.readAsText(err.error);
          } else {
              reject('not json')
          }
    })
  }

this.httpClient.get('http://m502126:3000/reports/perdate', {
observe: 'body',
responseType: 'blob',
params: new HttpParams().set('START_DATE', startDate)
 .set('END_DATE', endDate)
 .set('MIXER', mixer)
 .set('ATTACH', 'true')

 }).subscribe(data => {
 console.log(data);
},
err => {
     this.handleBlobError(err).then((errorAsJson)=> { // use the utility function here
     console.log(errorAsJson);
     const message = errorAsJson.message;
   })
}

Upvotes: 1

Radu Sebastian
Radu Sebastian

Reputation: 1

You can make the request in your service in this way:

 public fetchBlob(url, bodyParams): Observable<any> {
    return this.http.post(url, bodyParams, {
      observe: 'response',
      responseType: 'blob'
    }).pipe(
        switchMap((data: any) => {
          // <ResponseType>'application/json' is received when you need to show an error message
          if (data.body.type !== <ResponseType>'application/json') {
            return of(data);
          } else {
            var bufferPromise = data.body.arrayBuffer();

            return bufferPromise.then(buffer => {
              var enc = new TextDecoder("utf-8");
              return JSON.parse(enc.decode(buffer));
            });
          }
        })
    );

And in your component:

private fetchBlob(url) {
     this.yourService.fetchBlob(url).subscribe(
        (response) => {
            if (response.body instanceof Blob) {
                this.doSomethingWithBlob(response);
            } else {
                this.showError(response);
            }
        }
    );
}

Upvotes: 0

user1928596
user1928596

Reputation: 1763

In my case I added an interceptor for the error to be available for other interceptors and subscribers, code looks like this:

  intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(request).pipe(
      // until https://github.com/angular/angular/issues/19148 is fixed
      // can't update httpErrorResponse.error since it's readonly so creating a new httpErrorResponse to be available for others
      catchError((err: HttpErrorResponse) => {
        if (request.responseType === 'blob' && err.error instanceof Blob) {
          return from(Promise.resolve(err).then(async x => { throw new HttpErrorResponse({ error: JSON.parse(await x.error.text()), headers: x.headers, status: x.status, statusText: x.statusText, url: x.url ?? undefined })}));
        }
        return throwError(err);
      })

Upvotes: 0

Mazz
Mazz

Reputation: 1879

Struggled with this today to extract the json error from a blob response.

async error => {
    const jsonError = await (new Response(error.error)).json();
    await this.messageService.showErrorToast(jsonError.message);
}

This was the easiest solution i came up with. Source https://developer.mozilla.org/en-US/docs/Web/API/Blob#extracting_data_from_a_blob

Upvotes: 7

Wojciech Marciniak
Wojciech Marciniak

Reputation: 325

Basing on the accepted answer I use a uniform function for downloading things (usually served as a byte array) within a service, which can be injected to any component and make the function ready to use globally, to avoide boilerplate.

getAndSave(url: string, filename: string, mime: string, params?: HttpParams) {
            this.http.get(url, {responseType: "blob", params: params}).subscribe((content: any) => {
              const blob = new Blob([content], { type: mime });
              saveAs(blob, filename);
            }, async (error) => {
              const message = JSON.parse(await error.error.text()).message;
              this.toast.error(message, 'Error');
            });
          }

At the same time, as I handle errors globally in an interceptor, an exclusion had to be put there:

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
                const clonedRequest = req.clone({
                  withCredentials: true, // depending on needs
                });
            
                return next.handle(clonedRequest).pipe(
                  catchError((err: any) => {
                    if(err instanceof HttpErrorResponse) {
                      if (!(err.error instanceof Blob)) { // it's here
                        this.toast.error(err.error, 'Error');  
                      }
                    }
                    return of(err);
                  }));
    }

Basic usage:

export class MyComponent {
    
    constructor(private service: GlobalService) {}
    
    download() {
        this.service.getAndSave('https://example.com/report', 'Report.pdf', 'application/pdf'); 
    }
}  

Upvotes: 0

Marian Turchyn
Marian Turchyn

Reputation: 559

It can also be done with: error.text()

this.dataService
  .getFile()
  .subscribe((response) => {
    FileSaver.saveAs(response.body, 'file.txt');
  }, async (error) => {
    const message = JSON.parse(await error.error.text()).message;

    this.toast.error(message, 'Error');
  });

Upvotes: 30

nikos1351
nikos1351

Reputation: 71

If you're usung RxJS you can use something like this:

catchError((response: HttpErrorResponse) => {
  return !!this.isBlobError(response) ? this.parseErrorBlob(response) : throwError(response);
})

and after that you can chain other catchError and do your stuff.

Here are the methods:

isBlobError(err: any) {
  return err instanceof HttpErrorResponse && err.error instanceof Blob && err.error.type === 'application/json';
}

parseErrorBlob(err: HttpErrorResponse): Observable<any> {
  const reader: FileReader = new FileReader();
  const obs = new Observable((observer: any) => {
    reader.onloadend = (e) => {
      observer.error(new HttpErrorResponse({
        ...err,
        error: JSON.parse(reader.result as string),
      }));
      observer.complete();
    };
  });
  reader.readAsText(err.error);
  return obs;
}

Upvotes: 1

troYman
troYman

Reputation: 1808

The code from SplitterAlex worked for me but I needed the error object and the status code too. Thats why I adjusted the parseErrorBlob method a little bit.

public parseErrorBlob(err: HttpErrorResponse): Observable<any> {
const reader: FileReader = new FileReader();
const obs = new Observable((observer: any) => {
  reader.onloadend = (e) => {
    const messageObject = JSON.parse(reader.result as string);
    observer.error({
      error : {
        message : messageObject.message
      },
      message : messageObject.message,
      status : err.status
    });
    observer.complete();
  };
});
reader.readAsText(err.error);
return obs;

}

Upvotes: 4

SplitterAlex
SplitterAlex

Reputation: 2823

I was facing the same issue. In order to handle error response from a blob request you have to parse your error content via FileReader

This is a known Angular Issue and further details can be read there. You can find different solutions for your problem there as well.

For Example you can use this function to parse your error in JSON:

  parseErrorBlob(err: HttpErrorResponse): Observable<any> {
    const reader: FileReader = new FileReader();

    const obs = Observable.create((observer: any) => {
      reader.onloadend = (e) => {
        observer.error(JSON.parse(reader.result));
        observer.complete();
      }
    });
    reader.readAsText(err.error);
    return obs;
}

and use it like this:

public fetchBlob(): Observable<Blob> {
  return this.http.get(
    'my/url/to/ressource',
    {responseType: 'blob'}
  ).pipe(catchError(this.parseErrorBlob))
}  

Upvotes: 41

Related Questions