Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http ResponseType cannot be set #18586

Open
zaiddabaeen opened this issue Aug 8, 2017 · 59 comments
Open

Http ResponseType cannot be set #18586

zaiddabaeen opened this issue Aug 8, 2017 · 59 comments

Comments

@zaiddabaeen
Copy link

@zaiddabaeen zaiddabaeen commented Aug 8, 2017

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

@zaiddabaeen
Copy link
Author

@zaiddabaeen zaiddabaeen commented Aug 8, 2017

A workaround:

const options: {
            headers?: HttpHeaders,
            observe?: 'body',
            params?: HttpParams,
            reportProgress?: boolean,
            responseType: 'text',
            withCredentials?: boolean
        } = {
            headers: headers,
            params: params,
            responseType: 'text'
        };
@vicb vicb added the comp: http label Aug 8, 2017
@alxhub
Copy link
Contributor

@alxhub alxhub commented Aug 9, 2017

@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();
@zaiddabaeen
Copy link
Author

@zaiddabaeen zaiddabaeen commented Aug 10, 2017

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.

@alxhub
Copy link
Contributor

@alxhub alxhub commented Aug 17, 2017

@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.

@chrillewoodz
Copy link

@chrillewoodz chrillewoodz commented Sep 6, 2017

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
Copy link
Author

@zaiddabaeen zaiddabaeen commented Sep 6, 2017

@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.

@chrillewoodz
Copy link

@chrillewoodz chrillewoodz commented Sep 6, 2017

@zaiddabaeen I'm not casting anything to json (as far as I'm aware). This is what I have:

  public get<T>(url: string, params?: {[key: string]: any}, headers?: HttpHeaders): Observable<T> {
    return this.http.get<T>(this.config.host + url, this.getRequestOptions(params, headers));
  }

  private getRequestOptions(params?: any, customHeaders?: HttpHeaders) {

    let defaultHeaders: HttpHeaders = new HttpHeaders();

    defaultHeaders = defaultHeaders.set('Content-Type', 'application/json');

    return {
      headers: customHeaders || defaultHeaders,
      params: params ? this.convertJSONtoParams(params) : null
    };
  }

Attempting to add responseType: 'text' to the return of getRequestOptions is what throws the error.

@zaiddabaeen
Copy link
Author

@zaiddabaeen zaiddabaeen commented Sep 6, 2017

Use responseType: 'text' as 'text'

@chrillewoodz
Copy link

@chrillewoodz chrillewoodz commented Sep 6, 2017

None of the workarounds above works.

I have already tried all of the above :|

@roddy
Copy link

@roddy roddy commented Sep 20, 2017

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)
@yusijs
Copy link

@yusijs yusijs commented Sep 22, 2017

The only way I got it to work without error was using @roddy's example, and that's with inlined options...

This does not work:
image

and neither will:
image

Angular v4.4.3

@reppners
Copy link

@reppners reppners commented Sep 27, 2017

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.

@a-kolybelnikov
Copy link

@a-kolybelnikov a-kolybelnikov commented Oct 10, 2017

@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.

@btory
Copy link

@btory btory commented Oct 13, 2017

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

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

@akolybelnikov
Copy link

@akolybelnikov akolybelnikov commented Oct 13, 2017

@btory thank you. I have tried and it won’t work.

@rds-rafael
Copy link

@rds-rafael rds-rafael commented Oct 14, 2017

try this: .. , responseType: 'text' as 'json'

@robert-king
Copy link

@robert-king robert-king commented Nov 18, 2017

thanks @rds-rafael , this.http.get<ArrayBuffer>(this.pdfLink, {responseType: 'arraybuffer' as 'json'}) worked for me, a bit strange.

semmasodo pushed a commit to project-clara/project-clara that referenced this issue Nov 20, 2017
Modified option observe from 'response' to 'reponse' as 'response'.
Static typing issues...
see angular/angular#18586
toxitic added a commit to project-clara/project-clara that referenced this issue Nov 21, 2017
* feat(ui, general): adapt backend behaviour for authentication service

changing authentication.service and tests

* fix(client authentication): POst Options modified

Modified option observe from 'response' to 'reponse' as 'response'.
Static typing issues...
see angular/angular#18586
@alxhub
Copy link
Contributor

@alxhub alxhub commented Nov 28, 2017

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

@alxhub alxhub closed this Nov 28, 2017
@MathiasCiarlo
Copy link

@MathiasCiarlo MathiasCiarlo commented Dec 6, 2017

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

@claygifford
Copy link

@claygifford claygifford commented Dec 14, 2017

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

@nortain
Copy link

@nortain nortain commented Dec 19, 2017

@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.

@alxhub
Copy link
Contributor

@alxhub alxhub commented Dec 19, 2017

@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.

@nortain
Copy link

@nortain nortain commented Dec 19, 2017

@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.

@uzumakinaruto123
Copy link

@uzumakinaruto123 uzumakinaruto123 commented Dec 24, 2017

@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.

@Pastafarian
Copy link

@Pastafarian Pastafarian commented Jan 8, 2018

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.

@inthegarage
Copy link

@inthegarage inthegarage commented Jan 11, 2018

@Pastafarian I agree, in one case I see (with the same responseType option) that:

return this.httpClient.request('post', '/login', options)

Does work, this on the other hand does not:

return this.httpClient.post('/login', options)

Not sure I agree this httpclient is any easier or more intuitive than the previous incarnation. On the contrary it's clunky and relies on a lot of under the covers inference.

@VicXXX
Copy link

@VicXXX VicXXX commented Aug 24, 2018

Am I missing something here? I just encountered this error myself I expect responseType blob.

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

I receive type error but if I do

const httpOptions = {
      ...
      responseType:'blob' as 'blob'
    };

It works just fine. Am I wrong thinking this is ridiculous?

Answer that this is by design is not really a good answer it looks like a poor design to me if I can create HttpRequest where that param works as expected I'd expect it to work the same everywhere. Taking that docs don't mention this hacky approach anywhere.

Granted I am new to TypeScript but it just doesn't seem right I have to resolve to workarounds like this is this a TypeScript issue or Angular?

alxhub added a commit to alxhub/angular that referenced this issue Aug 24, 2018
HttpClient uses overloaded signatures in TypeScript to determine return
types for HttpClient calls based on two pieces of information: what the
response type from the server will be, and whether the caller is
interested in events, the full response, or just the response body.

Previously this was accomplished by specifying 'responseType' and
'observe' properties of the 'options' argument to HttpClient as string
values. This has a few disadvantages:

1) String literal values default to the 'string' type if they're not
passed directly to a function signature that will narrow them.

2) JSON values returned from the server are only cast to the expected
type, an assumption which is not checked at runtime.

This commit adds new options for specifying both 'responseType' and
'observe'.

responseType: a new type HttpResponseType captures both the expected
response type and a function which will assert that the type is correct
at runtime. Type inference can be used to determine the response type
from the function. The HttpResponseType class has static functions on
it for the 4 expected response types.

observe: HttpObserve has a similar object, which returns strings
narrowed to the right type.

Fixes angular#18586.
@alxhub alxhub reopened this Aug 24, 2018
@LimeyTX
Copy link

@LimeyTX LimeyTX commented Aug 30, 2018

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 might work, but it didn't. Eventually I found this thread, saw that I had to take the 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!

@VicXXX
Copy link

@VicXXX VicXXX commented Aug 30, 2018

Actually on a second thought after initial bafflement it does makes sense to me now as in
const httpOptions = { ... responseType:'blob' as 'blob' };
is not the same as inserting literal notation in the httpClient as @alxhub mentioned above somewhere.

@IgorMinar IgorMinar added comp: http and removed comp: http labels Sep 4, 2018
@ngbot ngbot bot added this to the needsTriage milestone Sep 4, 2018
marclabud pushed a commit to marclabud/behandlungsjournal that referenced this issue Oct 23, 2018
@Waseem-farooqui
Copy link

@Waseem-farooqui Waseem-farooqui commented Oct 31, 2018

What I did in case of blob type it works fine with no error in Angular 6.
I tried with both responseType: 'blob' as 'blob' but getting the syntax error and unable to build it.

const options = {headers: this.httpHeaders, responseType: 'blob' as 'json'};
return this.http.get<Blob>(Constants.BASE_URL + Constants.DOWNLOAD_FILE + path, options
)
@maleonix
Copy link

@maleonix maleonix commented Nov 6, 2018

Working for me

_downloadWeb(url, api, mimeType) { var token = getTokenWeb(); let data = { "siteId": this.state.idSite } Axios.defaults.headers.common['Authorization'] =${token}Axios.defaults.headers.common['Accept'] =${mimeType}Axios.defaults.headers.common['Content-Type'] =application/json Axios.post(url + api, data, { responseType: 'blob', }) .then((response) => { let blob = new Blob([response.data], { type: mimeType }); saveAs(blob, guid() + ".xlsx") }) .catch(function (error) { console.log("_downloadWeb", error); });; }

@janfeyen
Copy link

@janfeyen janfeyen commented Nov 6, 2018

adding the responseType: 'blob' implicitly changes the result to Observable / Obserrvable<HttpResponse>

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

@FrancescoBorzi
Copy link

@FrancescoBorzi FrancescoBorzi commented Dec 2, 2018

this issue is still happening in Angular 7.1

@rgoupil
Copy link

@rgoupil rgoupil commented Dec 17, 2018

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

#18586 (comment) 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.

@ngbot ngbot bot modified the milestones: needsTriage, Backlog Dec 18, 2018
@alxhub
Copy link
Contributor

@alxhub alxhub commented Dec 18, 2018

@rgoupil this is a cool idea!

@aipirvu
Copy link

@aipirvu aipirvu commented Feb 1, 2019

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);
@wunaidage
Copy link

@wunaidage wunaidage commented Mar 11, 2019

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

JagdCake added a commit to JagdCake/app.mock-users that referenced this issue Mar 17, 2019
@ocawa
Copy link

@ocawa ocawa commented Apr 17, 2019

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

@kippllo
Copy link

@kippllo kippllo commented Jun 14, 2019

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

@MartinX3
Copy link

@MartinX3 MartinX3 commented Jun 14, 2019

Working with Angular 7 (8 not tested yet)

import { HttpClient, HttpHeaders, HttpResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

@Injectable()
export class RequestService {
  httpOptions = {
    headers: new HttpHeaders({
      Accept: 'application/json;charset=utf-8',
      Authorization: `Basic ${btoa('user:password')}`,
      'Content-Type': 'application/json;charset=utf-8'
    }),
    observe: 'response' as 'body'
  };

  constructor(private readonly httpClient: HttpClient) {
  }

  get<T>(uri: string): Observable<HttpResponse<T>> {
    return this.httpClient.get<HttpResponse<T>>(uri, this.httpOptions);
  }

  post<T>(uri: string, value: T): Observable<HttpResponse<T>> {
    return this.httpClient.post<HttpResponse<T>>(uri, value, this.httpOptions);
  }

  put<T>(uri: string, value: T): Observable<HttpResponse<T>> {
    return this.httpClient.put<HttpResponse<T>>(uri, value, this.httpOptions);
  }
}

@juan98mg
Copy link

@juan98mg juan98mg commented Sep 3, 2019

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

You can’t perform that action at this time.