angular: HttpClient should allow different responseTypes for success response and error response

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[ x ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Currently, HttpClient expects the same responseType for both, success responses as well as error responses. This brings up issues when a WEB API returns e. g. JSON but just an (non JSON based) error string in the case of an error.

Expected behavior

In see two solutions: Provide an own errorResponseType field or assign the received message as a string when JSON parsing does not work (as a fallback)

Minimal reproduction of the problem with instructions

The following test case is using such an Web API. It returns json for the success case and plain text in the case of an error. Both tests lead to an 400 error due to validation issues. In the first case where we are requesting plain text, the text based error message is shown; in the second case where we are requesting json (which would be the case if the call succeeded) we are just getting null.

import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpClientModule, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';

fdescribe('Different responseTypes for success and error case', () => {

  let http: HttpTestingController;
  let httpClient: HttpClient;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientModule ]
    });

    httpClient = TestBed.get(HttpClient);
  });

  it('should send an HttpErrorResponse where the error property is the received body', (complete: Function) => {
    let actualBody;
    httpClient.post('http://www.angular.at/api/flight', {}, { responseType: 'text' }).subscribe(null, (resp: HttpErrorResponse) => {
      actualBody = resp.error;
      expect(actualBody).not.toBeNull();
      complete();
    });
  });

  it('should send an HttpErrorResponse where the error property is the body of the flushed response', (complete: Function) => {
    let actualBody;
    httpClient.post('http://www.angular.at/api/flight', {}, { responseType: 'json' }).subscribe(null, (resp: HttpErrorResponse) => {
      actualBody = resp.error;
      expect(actualBody).not.toBeNull();
      complete();
    });
  });

});

What is the motivation / use case for changing the behavior?

Using existing Web APIs more seamlessly

Environment


Angular version: 4.3


For Tooling issues:
- Platform:  Windows

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 53
  • Comments: 20 (3 by maintainers)

Most upvoted comments

A year later with Angular 6 still an issue. In my opinion this is a bug, not a feature.

Hello in 2023 😀 With Angular 15. Still the same stuff

Hello in 2024… Angular 17… still having this issue. 😅

My workaround is to add an interceptor with:

return next.handle(request).pipe(
      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);
      }),

Hello in 2022. This is still a problem, 5 years later…

@trotyl See this example. Spring @RestController handler method.

@GetMapping(
		path = "/export/xls",
		produces = MediaType.APPLICATION_OCTET_STREAM
)
public ResponseEntity<Resource> exportXls() {
	final var file = ....;
	final var httpHeaders = new HttpHeaders();
	httpHeaders.add(
			HttpHeaders.CONTENT_DISPOSITION,
			"attachment; filename=\"" + file.getName() + "\""
	);

	return ResponseEntity.ok()
	                     .headers(httpHeaders)
	                     .body(new FileSystemResource(file));
}

This returns a Blob object, basically. Then, the Angular HttpClient part.

return this.httpClient
	.get(url, { observe: 'response', responseType: 'blob' })
	.pipe(catchError(e => handleError(e)))

This works if the request is successful. However, in case of exception thrown from the Spring handler method, the body is set to Blob.

image

The Spring ExceptionHandler is pretty standard, and should produce a JSON body.

@ResponseStatus(HttpStatus.BAD_REQUEST)
Result<Void> handleBadRequest(final Exception exception) {
	logger.error(exception.getMessage(), exception);
	return new Result<>(1, exception.getMessage());
}

Hey peps ✌️ I also ran into the same issue yesterday.

My app is requesting a ZIP from the API, but receives JSON in case of an error. I was wondering if responseType could be conditional based on the Content-Type of the response.

type ResponseType = 'arraybuffer' | 'blob' | 'json' | 'text'
{
  responseType: ResponseType | {[key: string]: ResponseType}
}
get<T>(url: string, options: {
    headers?: HttpHeaders | {
        [header: string]: string | string[];
    };
    context?: HttpContext;
    observe?: 'body';
    params?: HttpParams | {
        [param: string]: string | number | boolean | ReadonlyArray<string | number | boolean>;
    };
    reportProgress?: boolean;
    responseType: {[key: string]: ResponseType},
    withCredentials?: boolean;
}): Observable<T>;

The requesting code would look something like this:

this.http.get('{url}', {
  responseType: {
    'application/json': 'json',
    'application/zip': 'arraybuffer',
    '*/*': 'text',
  },
})

For me, I use a synchronous way to convert Blob to json. I wrote a post about it here: https://stackoverflow.com/questions/48500822/how-to-handle-error-for-response-type-blob-in-httprequest/70977054#70977054

This is how it works:

//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;
}
// test case
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'}.