91

Usually it's desirable to have default timeout (e.g. 30s) that will be applied to all requests and can be overridden for particular longer requests (e.g. 600s).

There's no good way to specify default timeout in Http service, to my knowledge.

What is the way to approach this in HttpClient service? How to define a default timeout for all outgoing requests, that can be overriden for specific ones?

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
Estus Flask
  • 206,104
  • 70
  • 425
  • 565

4 Answers4

181

It appears that without extending HttpClientModule classes, the only expected ways for interceptors to communicate with respective requests are params and headers objects.

Since timeout value is scalar, it can be safely provided as a custom header to the interceptor, where it can be decided if it's default or specific timeout that should be applied via RxJS timeout operator:

import { Inject, Injectable, InjectionToken } from '@angular/core';
import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Observable } from 'rxjs';
import { timeout } from 'rxjs/operators';

export const DEFAULT_TIMEOUT = new InjectionToken<number>('defaultTimeout');

@Injectable()
export class TimeoutInterceptor implements HttpInterceptor {
  constructor(@Inject(DEFAULT_TIMEOUT) protected defaultTimeout: number) {
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const timeoutValue = req.headers.get('timeout') || this.defaultTimeout;
    const timeoutValueNumeric = Number(timeoutValue);

    return next.handle(req).pipe(timeout(timeoutValueNumeric));
  }
}

This can be configured in your app module like:

providers: [
  [{ provide: HTTP_INTERCEPTORS, useClass: TimeoutInterceptor, multi: true }],
  [{ provide: DEFAULT_TIMEOUT, useValue: 30000 }]
],

The request is then done with a custom timeout header

http.get('/your/url/here', { headers: new HttpHeaders({ timeout: `${20000}` }) });

Since headers are supposed to be strings, the timeout value should be converted to a string first.

Here is a demo.

Credits go to @RahulSingh and @Jota.Toledo for suggesting the idea of using interceptors with timeout.

Michael Ziluck
  • 599
  • 6
  • 19
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • @Jota.Toledo I don't think observables alone can help here. Once an observable was chained with`.timeout(defaultTimeout)` operator inside the interceptor, it's impossible to 'cancel' it and chain with `.timeout(customTimeout)` instead. This is probably possible by subclassing Observable but it will be cumbersome and fragile too. Hope this will be fixed someday in HttpClient itself. AngularJS $http had timeout option and it worked just great. – Estus Flask Aug 31 '17 at 17:53
  • Yup, Im aware of that. I tried to implement something with mergeMap and other operators based in a SO answer, but In one of the cases (I think default overriden by larger time) my approach didnt work. I will choose your approach despite the fact that I dont like the use of HttpHeader for comunication with the interceptor, but in the current state of the API I think there is no better approach. – Jota.Toledo Sep 06 '17 at 06:33
  • Yes, since there are no other options to interact with interceptor, headers look like the only one, and it's not really bad. At least the semantics is correct, headers are suppose to carry information about a request. Hope this will be fixed in future versions. – Estus Flask Sep 06 '17 at 10:12
  • 1
    this code doesn't work to increase timeout request to be bigger than 30s. If you set it to 60 seconds, the angular's default 30 seconds will be applied – Thompson Jan 14 '19 at 11:46
  • after adding timeout , do we have any additional parameter sent from UI to backend ? – sparsh610 Apr 04 '19 at 15:17
  • @sparsh610 I'm not sure if I understood you correctly, but a request itself doesn't differ. A timeout forces a request to be closed after specified amount of time if there was no response from a server. – Estus Flask Apr 04 '19 at 15:30
  • @sparsh610 Yes, but only if a server allows that. – Estus Flask Apr 04 '19 at 15:39
  • @estus I am trying to use this as it appears to solve all my woes, but I am new to the InjectionToken class and am running into the error `StaticInjectorError...No provider for InjectionToken defaultTimeout!`. I know it's a longshot, but would you have any idea you can share as to why this is happening? – Ross Brasseaux May 07 '19 at 19:25
  • @Lopsided Means that you didn't register it as a provider, so you can't inject it. See the part with `provide: DEFAULT_TIMEOUT ...`. – Estus Flask May 07 '19 at 19:28
  • @EstusFlask I just wanted to give you props for your answer AND your nickname. Praise the sun! – bl4ckb0l7 Oct 31 '19 at 08:07
  • @EstusFlask Why doesn't your default value of 30000 have to be a string? Is typescript automatically converting that in the useValue property? – Willie Aug 19 '21 at 18:26
  • @Willie TS doesn't do any magic at runtime besides the behaviour expected from relevant ES specs. Default timeout is a custom service that isn't used anywhere but in this snippet, so it can be any type we need. Here timeout value needs to be converted into a number in order to be used with `timeout` any way, making it `"30000"` instead of `30000` would work the same way but require 2 characters more to type. – Estus Flask Aug 20 '21 at 07:35
  • 2022 is this still valid? – user1034912 Feb 07 '22 at 01:40
  • 1
    @user1034912 Afaik yes, Angular doesn't provide anything more specific to implement this – Estus Flask Feb 07 '22 at 06:16
30

In complement to the other answers, just beware that if you use the proxy config on the dev machine, the proxy's default timeout is 120 seconds (2 minutes). For longer requests, you'll need to define a higher value in the configuration, or else none of these answers will work.

{
  "/api": {
    "target": "http://localhost:3000",
    "secure": false,
    "timeout": 360000
  }
}
Marcos Dimitrio
  • 6,651
  • 5
  • 38
  • 62
16

You could create a global interceptor with the base timeout value as follows:

import { Injectable } from '@angular/core';
import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest} from '@angular/common/http';

@Injectable()
export class AngularInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).timeout(30000, Observable.throw("Request timed out"));
    // 30000 (30s) would be the global default for example
  }
}

Afterwards you need to register this injectable in the providers array of you root module.

The tricky part would be to override the default time (increase/decrease) for specific requests. For the moment I dont know how to solve this.

Jota.Toledo
  • 27,293
  • 11
  • 59
  • 73
15

Using the new HttpClient you can try some thing like this

@Injectable()
export class AngularInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).timeout(5000).do(event => {}, err => { // timeout of 5000 ms
        if(err instanceof HttpErrorResponse){
            console.log("Error Caught By Interceptor");
            //Observable.throw(err);
        }
    });
  }
}

Adding a timeout to the next.handle(req) which is passed on.

Registering it in AppModule like

@NgModule({
    declarations: [
        AppComponent
    ],
    imports: [
        BrowserModule,
        HttpClientModule
    ],
    providers: [
        [ { provide: HTTP_INTERCEPTORS, useClass: 
              AngularInterceptor, multi: true } ]
    ],
    bootstrap: [AppComponent]
})
export class AppModule {
}
Rahul Singh
  • 19,030
  • 11
  • 64
  • 86
  • Thanks. However, as mentioned in another answer, it's seems to be impossible to increase timeout for particular request this way. – Estus Flask Aug 29 '17 at 14:12
  • 1
    @estus yes that is the hard part as of now I don't think there is any direct solutions to it might have a work around. Will see to it. The other answer was not much different. Just added a throw after time out – Rahul Singh Aug 29 '17 at 14:15