angular: Http ResponseType cannot be set

I’m submitting a…


[ ] Regression (a behavior that used to work and stopped working in a new release)
[x] Bug report  
[ ] 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

Response type cannot be set for HttpClient methods.

        const options = {headers: headers, params: params, responseType: 'text'};

        return this.http.get(url, options).share();

Would show an error

  Types of property 'responseType' are incompatible.
    Type 'string' is not assignable to type '"json"'.

Expected behavior

It is expected that the response type should be exported like

export type ResponseType = 'arraybuffer' | 'blob' | 'json' | 'text';;

And one would be able to set it using this type. Otherwise the type cannot be changed.

Environment

Angular version: 4.1.1 and still there in 5.0.0-beta.2 as seen here: https://github.com/angular/angular/blob/master/packages/common/http/src/client.ts

About this issue

  • Original URL
  • State: open
  • Created 7 years ago
  • Reactions: 122
  • Comments: 71 (8 by maintainers)

Commits related to this issue

Most upvoted comments

@zaiddabaeen currently, this is by design. Typescript needs to be able to infer the observe and responseType values statically, in order to choose the correct return type for get(). If you pass in an improperly typed options object, it can’t infer the right return type.

Another workaround is:

const options = {headers, params, responseType: 'text' as 'text'};
return this.http.get(url, options).share();

Closing, the solution is to pass a correctly typed argument.

So instead we have to do workarounds to get the desired effect? That can’t be the way to go about this. I’ve been screaming at my computer for quite a while over this now, I’ve got a service which wraps the HttpClient but trying to set responseType doesn’t work, the only way that I can get the error to go away is by doing responseType: 'text' as 'json'… None of the workarounds above works.

@zaiddabaeen the problem is for:

const res = this.http.get(url, options);

What is the type of res? It depends on the values in options - but Typescript has no way to know what those values are if it’s not inlined.

In other words:

const res = this.http.get(url, {responseType: 'text'});

is not equivalent to

const options = {responseType: 'text'};
const res = this.http.get(url, options);

In the first one Typescript can infer that the type of res is Observable<string>, in the second one it cannot be determined via type inference. If we added this feature, we would have to return an Observable<any> which would be a poor experience.

I expect most cases where this is desired can be solved with the spread operator:


// Some options we want to control dynamically.
const options = {headers: ..., params: ...};
const res = this.http.get(url, {...options, responseType: 'text'});

This way Typescript can infer the return type based on the signature and the value of responseType, but options can also be passed in without reconstructing the whole object.

I understand, but I believe that’s unintuitive and confusing to the developers. I don’t recall I ever casted a string to a ‘string’ before. Enumerating and using types as suggested would sound to me as a cleaner solution.

I also had this problem, but by removing the <T> from the get call I was able to get it to work using responseType: 'text'as 'text'.

Eg this does not work and returns the error:

const options: { responseType: 'text' as 'text', withCredentials: true };
this.httpClient.get<string>(url, options)

But this does work:

const options: { responseType: 'text' as 'text', withCredentials: true };
this.httpClient.get(url, options)

A workaround:

const options: {
            headers?: HttpHeaders,
            observe?: 'body',
            params?: HttpParams,
            reportProgress?: boolean,
            responseType: 'text',
            withCredentials?: boolean
        } = {
            headers: headers,
            params: params,
            responseType: 'text'
        };

Use responseType: 'text' as 'text'

try this: … , responseType: ‘text’ as ‘json’

The generic MUST not be used when responseType is specified to something other than json because typeof T will then be inferred automatically.

Take a look at how it is defined for the get() method

    /**
     * Construct a GET request which interprets the body as an `ArrayBuffer` and returns it.
     *
     * @return an `Observable` of the body as an `ArrayBuffer`.
     */
    get(url: string, options: {
        headers?: HttpHeaders;
        observe?: 'body';
        params?: HttpParams;
        reportProgress?: boolean;
        responseType: 'arraybuffer';
        withCredentials?: boolean;
    }): Observable<ArrayBuffer>;
    /**
     * Construct a GET request which interprets the body as a `Blob` and returns it.
     *
     * @return an `Observable` of the body as a `Blob`.
     */
    get(url: string, options: {
        headers?: HttpHeaders;
        observe?: 'body';
        params?: HttpParams;
        reportProgress?: boolean;
        responseType: 'blob';
        withCredentials?: boolean;
    }): Observable<Blob>;
    /**
     * Construct a GET request which interprets the body as text and returns it.
     *
     * @return an `Observable` of the body as a `string`.
     */
    get(url: string, options: {
        headers?: HttpHeaders;
        observe?: 'body';
        params?: HttpParams;
        reportProgress?: boolean;
        responseType: 'text';
        withCredentials?: boolean;
    }): Observable<string>;
    /**
     * Construct a GET request which interprets the body as JSON and returns it.
     *
     * @return an `Observable` of the body as an `Object`.
     */
    get(url: string, options?: {
        headers?: HttpHeaders;
        observe?: 'body';
        params?: HttpParams;
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
    }): Observable<Object>;
    /**
     * Construct a GET request which interprets the body as JSON and returns it.
     *
     * @return an `Observable` of the body as type `T`.
     */
    get<T>(url: string, options?: {
        headers?: HttpHeaders;
        observe?: 'body';
        params?: HttpParams;
        reportProgress?: boolean;
        responseType?: 'json';
        withCredentials?: boolean;
    }): Observable<T>;

This makes perfect sense since the type can only be something angular does not know statically when the responseType is json - all other cases there is no need for a generic.

So whoever won the argument to re-factor Http to HttpClient this way is just wrong.

None of the workarounds above works.

I have already tried all of the above 😐

For blobs: .get(url, { 'responseType: 'blob' as 'json' })

Cast the httpOptions variable to type Object:

const httpOptions: Object = {
  responseType: 'blob'
};

as specified in the docs:https://angular.io/api/common/http/HttpClient#get

@chrillewoodz’s solution worked for me I had something like this.httpClient.post<string>(url, data, {responseType: 'text' as 'json'}); and this worked as the api I was called was simply returning a string. Of course, removing the generic means that the whole thing needed to be changed to:
this.httpClient.post(url, data, {responseType: 'text' as 'text'});

While I find the workaround to this to only be a minor annoyance it was really furustring that it was so difficult to actually find this answer. It’d be nice if this was better documented or I suppose a more intuitive solution be reached.

All feels very convoluted to me. I’d rather have different methods to call for different results rather than the inference which is currently being used.

I eventually came up with this and though I could share it:

// define this namespace somewhere
namespace ResponseType {
    export const JSON = 'json' as 'json';
    export const ArrayBuffer = 'arraybuffer' as 'arraybuffer';
    export const Blob = 'blob' as 'blob';
    export const Text = 'text' as 'text';
}
// import the namespace above and use it like this
const reqOpts = {
    params: params,
    headers: headers,
    responseType: ResponseType.JSON,
};

// no type error, the right signature is selected
const a = await this.http.get(url, reqOpts);
const b = await this.http.get<MyClass>(url, reqOpts);

https://github.com/angular/angular/issues/18586#issuecomment-327440092 gave a good hint at how to implement this.

This solution also work for the observe parameter.


Unless I missed something, this could be implemented in Angular directly, isn’t ?

You would need to first put the following in the declaration @angular/common/http/src/client.d.ts:

declare module '@angular/common/http' {
    export namespace HttpResponseType {
        export const JSON: 'json';
        export const ArrayBuffer: 'arraybuffer';
        export const Blob: 'blob';
        export const Text: 'text';
    }

    export declare type HttpObserve = 'body' | 'events' | 'response';
    export namespace HttpObserve {
        export const Body: 'body';
        export const Events: 'events';
        export const Response: 'response';
    }
}

Then implement it in angular/packages/common/http/src/client.ts:

export namespace HttpResponseType {
    export const JSON: 'json' = 'json';
    export const ArrayBuffer: 'arraybuffer' = 'arraybuffer';
    export const Blob: 'blob' = 'blob';
    export const Text: 'text' = 'text';
}

export type HttpObserve = 'body' | 'events' | 'response';
export namespace HttpObserve {
    export const Body: 'body' = 'body';
    export const Events: 'events' = 'events';
    export const Response: 'response' = 'response';
}

Finally the two namespaces should be properly exported all the way back to @angular/common/http.

@alxhub Facing the same issue with observe property

this.http.get(endpoint, {
      observe: 'response'
    })
[ts]
Argument of type '{ headers: HttpHeaders; observe: string; }' is not assignable to parameter of type '{ headers?: HttpHeaders | { [header: string]: string | string[]; }; observe?: "body"; params?: Ht...'.
  Types of property 'observe' are incompatible.
    Type 'string' is not assignable to type '"body"'.

only observe: 'body' works. 'response' and 'events' does not.

@nortain you definitely don’t need either.

this.httpClient.post(url, data, {responsesType: 'text'})

will give you an Observable<string> without you having to specify <string>. You don’t need to say 'text' as 'text', Typescript knows that.

The only time you should be passing a type parameter to an HttpClient method is with responseType: 'json'. All of the others are implicit.

I’ve spent the better part of a day on trying to figure out why postman gave the correct response and http.get didn’t. I thought using http:get<string> might work, but it didn’t. Eventually I found this thread, saw that I had to take the <string> out and use responseType: ‘text’ as ‘text’ as roddy suggested back last September. Then I read to the end of the post and found that it is still a problem. I think this should be fixed, or at the very least documented because it’s far from intuitive!

Please mention this in:

https://angular.io/guide/http#requesting-non-json-data

My code:

return this.http.get<string>(this.apiURLDisplays+'/'+name, { responseType: 'text' })
            .pipe(
                catchError(this.handleError)
            );

I don’t get it though - ng serve blows up with:

ERROR in src/app/shared/api.service.ts(937,68): error TS2345: Argument of type '{ responseType: "text"; }' is not assignable to parameter of type '{ headers?: HttpHeaders | { [header: string]: string | string[]; }; observe?: "body"; params?: Ht...'.
  Types of property 'responseType' are incompatible.
    Type '"text"' is not assignable to type '"json"'.

But it then serves a page and it works fine in the browser… so is this just a typescript issue and it somehow still does get transpiled into javascript & works?

Enum would’ve been nicer though…

Also, I think the reason people are saying 'text' as 'json' works is in VSCode if you have get<string>() like mine above, it draws a red line under it if you have 'text' as 'text' but accepts 'text' as 'json'

I just read @alxhub 's comment about <string> being implicit in this case, took out <string> and VSCode accepted return this.http.get(this.apiURLDisplays+'/'+name, { responseType: 'text' as 'text' })

Seeing stuff like responseType: 'text' as 'text' feels broken to me. Maybe the whole Http options object should have been a well-defined interface with explicit enum types.

This is not well documented at all ( https://angular.io/api/common/http/HttpClient#post )

@a-kolybelnikov You can use something like this in your case:

.get(url, { headers: this.getHeaders(), responseType: ResponseContentType.Blob as 'blob' })

@chrillewoodz The default is JSON however. Why are you casting it to json? My approach works, and I can confirm that it is now run on production.

Just my 2 cents:

Please make this a documentation ticket. Everybody is already confused. I just discussed this issue with colleagues.

get() has 15 overloads, 3 of which are generic. request() has 17 overloads, 4 of which are generic.

I am yet to see a compiler that generates useful error messages in face of so many overloads.

The issue is that both responseType and observe are often required for proper resolution. This is hinted at in the docs of request(), but not get() or any other HTTP method. But even in request() docs are not really explicit about the requirement to select the right overload. Javascript devs typically do not pay much attention to overloading, and that’s a big part of the user base.

Maybe I’m misreading this, but in here the observe is optional. But in many cases typescript needs the information for overload resolution.

I think the design intends to keep people on the success path in terms of useful types. I wouldn’t “fix” that.

Just make it abundantly clear that the caller has to select the right overload, how to do so, and what to avoid.

I have to hack it with const options = {responseType: 'text' as 'json'}; this.httpClient.get<string>(url, options).subscribe()

@ghillert unfortunately that would suffer from the same problem. The type of the field in the interface would be the enum type, not the particular value. HttpClient’s type inference depends on the type of the object being passed to get() being narrowed to a specific value type.

There’s an easy solution to all this, though:

const options = {
  headers: ...,
  params: ...,
  reportProgress: false,
  withCredentials: true,
};

return this.http.get(url, {...options, responseType: 'text'});

Basically, you just have to specify the responseType and observe values directly in the call to get(). Everything else can go in the options object.

I also had this problem, but by removing the <T> from the get call I was able to get it to work using responseType: 'text'as 'text'.

Eg this does not work and returns the error:

const options: { responseType: 'text' as 'text', withCredentials: true };
this.httpClient.get<string>(url, options)

But this does work:

const options: { responseType: 'text' as 'text', withCredentials: true };
this.httpClient.get(url, options)

this solution works for me

@rgoupil this is a cool idea!

TS is easy))

    const headers = new HttpHeaders({
        'Authorization': 'Bearer ' + token
    });
    const params = new HttpParams[]
    const observe = 'response';
    const reportProgress = false;
    const responseType = 'text';
    const withCredentials = false;

    const options = {
        headers: headers,
        observe: <any>observe,
        params: params,
        reportProgress: reportProgress,
        responseType: <any>responseType,
        withCredentials: withCredentials
    };

    return this.http.get<HttpResponse>(href, options);

Same problem occurred over here… Adding ‘text’ as 'text solved it…

const httpOptions = {
            headers: new HttpHeaders({
              'Content-Type':  'application/json',
              'Accept': 'text/html'
            })
        };
return this.http.post<any>(url, { 'new': new_elements, 'old': old_elements }, {...httpOptions, responseType: 'text' as 'json' });

Thanks, @alxhub. I re-read your comment and I begin to understand what’s happening (should’ve read that more carefully).

I guess it’s my choice to choose between inline, not inline but casting, or not inline but using spread operator for the things that are type-dependent, is it? Basically, these three options:

login(username: string, password: string): Observable<string> {
  const body = `username=${username}&password=${password}`;
  return this.http.post(this.loginUrl, body, {
    headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }),
    responseType: 'text'
  });
}
login(username: string, password: string): Observable<string> {
  const body = `username=${username}&password=${password}`;
  const options = {
    headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }),
    responseType: 'text' as 'text'
  };
  return this.http.post(this.loginUrl, body, options);
}
login(username: string, password: string): Observable<string> {
  const body = `username=${username}&password=${password}`;
  const options = {
    headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }),
  };
  return this.http.post(this.loginUrl, body, { ...options, responseType: 'text' });
}

Is the as 'text' variant (second option) really a “workaround” as stated in this issue? Or is it just the way to do things? Is there a preferred option?

@tobihagemann responseType: 'text' as 'text' is what you want. 'text' is not 'json'.

@reppners Hi Stefan, thank you for the explanation above. However, no matter how I try I cannot make the new HttpClient to work in a service that I am using to intercept image urls in my app:

get(url: string): Observable<any> {
    return new Observable((observer: Subscriber<any>) => {
        let objectUrl: string = null;
          this.http
            .get(url, {headers: this.getHeaders(), responseType: ResponseContentType.Blob} )
            .subscribe(m => {
              objectUrl = URL.createObjectURL(m.blob());
              observer.next(objectUrl);
            });

        return () => {
          if (objectUrl) {
            URL.revokeObjectURL(objectUrl);
            objectUrl = null;
          }
        }
    });
  }
  getHeaders(): Headers {
    let headers = new Headers();

    let token = this.authService.getToken();
    if (token) {
      headers.set('Authorization', 'Bearer ' + token);
    }
    
    return headers;
  }

Neither responseType: 'blob' set directly in the GET request, nor setting it in the options: { } as mentioned above works.

Any news on this? I’ve been struggling with the same issue on HttpClient.post’s options…

I had the same problem. After a half an hour of trying to implement some of the suggestions above I wen’t ahead with the following (dirty but…):


// I actually get the response type as a parameter to my function
const responseType = 'text';

const options = {
    headers: new HttpHeaders()
};

// @ts-ignore
options['responseType'] = responseType;

this.http.get(url, options);

adding the responseType: ‘blob’ implicitly changes the result to Observable<Blob> / Obserrvable<HttpResponse<Blob>>

Not typing the httpClient.post or httpClient.get solves it for me

return this.httpClient.post<any>(
      `${environment.apiEndpoint}/${environment.apiVersion}/template-version/${templateId}/${version}/test`,
      { answers },
      { headers: this.blobHeaders, responseType: 'blob' })
      .pipe(
        tap(
          // Log the result or error
          data => console.log('You received data'),
          error => console.log(error)
        )
      );

gives the error, but

return this.httpClient.post(
      `${environment.apiEndpoint}/${environment.apiVersion}/template-version/${templateId}/${version}/test`,
      { answers },
      { headers: this.blobHeaders, responseType: 'blob' })
      .pipe(
        tap(
          // Log the result or error
          data => console.log('You received data'),
          error => console.log(error)
        )
      );

works

I simply added below and worked well in angular 5. const httpOptions = { headers: new HttpHeaders({ ‘Content-Type’: ‘Application/json; charset=UTF-8’ }), responseType: ‘text’ as ‘text’ }; const body = JSON.stringify(params); return this._http.post(url, body, httpOptions);

ok so here’s what worked for me :

actions.ts :

generatePOR(){
  this._api.generatePOR(this.selection).subscribe(res => {
    if(res !== null && res !== undefined){
      console.log(res.body);
    }
  }, (error) => console.log(error), () => {});
}

api.ts :

generatePOR(idList): any {
  const apiURL = `${this.API_URL}/purchaseorders/generatePOR`;
  this.PORresult = this._http.post(apiURL, idList, {
    observe: 'response',
    headers: new HttpHeaders({'Content-Type': 'application/json'}),
    responseType: 'text' as 'text'
  }).catch(this.handleError);
  return this.PORresult;
}

This was the key answer for me: https://github.com/angular/angular/issues/18586#issuecomment-323216764

You have to inline your options object in the call to get (or post), for the Typescript compiler not to complain about your return type.

The only way this compiles without complaining that res is not an ArrayBuffer is to inline the options like this:

this.http.post<AuthResult>(this.AuthenticationEndpoint, 
    body, { withCredentials: false }).subscribe(res => {

@alxhub Ah thank you, that makes more sense and I realize i hadn’t tried dropping the as ‘text’ when removing the generic. Also knowing that the generic is intended for json responses only helps. I appreciate the follow up.