Reputation: 1424
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
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
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
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
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
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
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
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
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
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
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
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
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
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