angular icon indicating copy to clipboard operation
angular copied to clipboard

HttpClient should allow different responseTypes for success response and error response

Open manfredsteyer opened this issue 8 years ago • 20 comments

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

manfredsteyer avatar Sep 11 '17 19:09 manfredsteyer

This would be really great! I have a Spring Service that usually returns an Excel file (HttpClient uses responseType 'blob'), but might return JSON with error details if the generation fails.

In case of an Error the HttpErrorResponse.error is undefined when handling it in HttpInterceptor, so I cannot use the additional informations returned by the Backend.

I don't see any good workaround for doing this now:

  • Move error messages from Backend to Frontend? -> Bad because only Backend knows all error details
  • Use 2 requests, one for the actual request and one to get the last error message? -> Bad
  • Don't use 'blob' but wrap the Response in JSON, encoding the blob content with Base64? -> More traffic, additional overhead in programming, linking to files directly ("/foo/bar.xls") wouldn't work anymore because JSON is returned.

So both thumbs up for this feature request!

abender avatar Nov 16 '17 08:11 abender

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

KlausHans avatar Sep 13 '18 13:09 KlausHans

Any news on this?

lppedd avatar Feb 12 '19 13:02 lppedd

@lppedd This issue has already been fixed by https://github.com/angular/angular/pull/19773 since 4.4.6, an error body will always fallback to string when unable to parse as JSON.

Please provide repro if you have different case other than OP.

trotyl avatar Feb 13 '19 04:02 trotyl

@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());
}

lppedd avatar Feb 13 '19 11:02 lppedd

@lppedd I see your case now, but the problem is, what Angular did is just pass responseType: 'blob' to XMLHttpRequest.responseType, and the parsing process is performed by browser.

The Blob your got is already the raw response provided by browser, regardless of statusCode, as there's no XMLHttpRequest.responseTypeOnSuccess and XMLHttpRequest.responseTypeOnError.

trotyl avatar Feb 13 '19 13:02 trotyl

@trotyl I "temporary" solved by integrating a blob > string transformation. Do you think Angular will ever introduce the responseTypeOnError HttpClient option?

lppedd avatar Feb 13 '19 13:02 lppedd

@lppedd I'd rather Angular drop the entire responseType concept of old XHR and switch to transform-style operation like fetch API, currently the static typing is already bloated with all this options and hard to control.

trotyl avatar Feb 13 '19 13:02 trotyl

Hello again in 2020, I have this issue but cannot use the temporary solution described here. The problem is I want to synchronously get the error to check if the jwt token really got expired and do a refersh call API. This cannot be done using FileReader, and FileReaderSync is only available in Workers. I'm trying to find a hackaround for this but it's really not promising. Please fix this issue.

MrNocTV avatar Jan 13 '20 05:01 MrNocTV

Also have had an issue with this. I will try workaround for now.

bhBio avatar Aug 04 '20 22:08 bhBio

isn't the success type solved with the generic HttpClient we got a while back? https://angular.io/api/common/http/HttpClient#get

image

That at least solves it for me.

However the error response type is still any | null. Please fix :) https://angular.io/api/common/http/HttpErrorResponse#error

Keep in mind that each http return code could return a different type. Http return code 400 and 500 probably doesn't have the same format.

snebjorn avatar Nov 17 '20 21:11 snebjorn

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 })}));
        }
        if (request.responseType === 'text' && typeof err.error === 'string') {
          return from(Promise.resolve(err).then(async x => { throw new HttpErrorResponse({ error: JSON.parse(await x.error), headers: x.headers, status: x.status, statusText: x.statusText, url: x.url ?? undefined })}));
        }
        return throwError(err);
      }),

EDIT: improved version that handles responseType 'text'. Also this interceptor should be added last since response intercepting is handled in reverse order.

yelhouti avatar Sep 29 '21 15:09 yelhouti

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);
      }),

Worked like a charm, thank you so much!

It would be great if Angular would have such a feature where you can set different response types for success or error responses.

Gummiees avatar Oct 06 '21 08:10 Gummiees

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

AlexanderTang avatar Feb 03 '22 19:02 AlexanderTang

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

thargenediad avatar Oct 04 '22 13:10 thargenediad

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

qliqdev avatar Jan 19 '23 03:01 qliqdev

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',
  },
})

zZeepo avatar Feb 25 '23 15:02 zZeepo

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',

  },

})

Nice suggestion. +1 for this

qliqdev avatar Mar 29 '23 16:03 qliqdev

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

arron21 avatar Feb 15 '24 19:02 arron21

yes this is very bad

sysmat avatar Mar 08 '24 09:03 sysmat

Angular 18.. how great it would be to have this issue solved

HannaKukharava avatar Jun 20 '24 13:06 HannaKukharava

This issue should be more visible in https://angular.dev if not already. Spent a while tracking down this bug in our app to end up seeing that it is an open Angular issue.

Will-at-FreedomDev avatar Jun 28 '24 16:06 Will-at-FreedomDev