4

I've got a very practical usable question which probably has some fancy Angular Dependency Injection solution.

Basically:

I have Interceptor A to provide an Auth Header for communicating with Service A.

And I have Interceptor B to provide an Auth Header for communicating with Service B.

How do I get every instance of Service A to have the Interceptor chain with Interceptor A? And how do I guarantee the opposite for Service B?

Both Services are used globally throughout the app so I can't don't think I can define a new HTTP_INTERCEPTOR Injection token only in the modules where Service B / A is used.

Thank you!

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
Alex
  • 422
  • 4
  • 12
  • interceptors are single to entire application. you can do the same inside interceptor. as per your conditoin you can manipulate data inside your interceptor – Aniruddha Das Sep 22 '17 at 00:19
  • 3
    That's kind of not wonderful. That means if you import an API client with its own interceptor it would add on to the application interceptors which would eventually balloon to many interceptors. I would imagine there is some way to request a different HTTP_INTERCEPTOR chain based on the some token you can specify in the constructor of the client API. Or maybe some special interceptor that reads a string off the request and applies a different set of interceptors. – Alex Sep 22 '17 at 00:34
  • It's pretty nasty to have to basically filter which requests I want to apply to in the interceptors themselves. I guess I could create some base class. But it's still not optimal. – Alex Sep 22 '17 at 00:36
  • agree! but is not it like a anti pattern – Aniruddha Das Sep 22 '17 at 01:00
  • I wonder if the angular developers, while reinventing Http client in Angular 4, did not consider an app having more than one API backend with different requirements. – Alex Che Jun 28 '18 at 13:55
  • May be this one is close: http://www.learn-angular.fr/how-can-we-have-multiple-instances-of-httpclient-instance-with-angular/ – Renil Babu Jul 03 '19 at 07:00

2 Answers2

3

Considering that both services are registered on root injector, the only way to do that is to create a new module that is similar to HttpClientModule but has new DI tokens - HttpClientB instead of HttpClient and HTTP_INTERCEPTORS_B instead of HTTP_INTERCEPTORS. This will requite to extract several internals from common/http/src because they aren't exported, e.g. interceptingHandler.

This way it's possible for service B to have its own injectors in HTTP_INTERCEPTORS and inject HTTP client as HttpClientB.

A simpler way is to have an interceptor that is able to switch between implementations depending on input. As shown here, the only ways to interact with interceptor during request is to pass data via request parameters or headers.

So service A can do requests like:

http.get(..., { headers: new HttpHeaders({ useAuth: 'A' }) });

And interceptor receives the header and makes a decision based on it:

  intercept(req, next) {
    if (req.headers.get('useAuth') === 'A') ...
    else if (req.headers.get('useAuth') === 'B') ...
    else ...

    req.headers = req.headers.delete('useAuth'));
  }

If interceptors A and B are specific only to services A and B but not to the rest of the application, interceptors shouldn't be used at all. Instead this should be done directly in services A and B.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • 1
    I marked this as the answer because this is basically what I ended up doing although it seems sort of like an Anti-Pattern because Interceptors are meant to de-couple logic from specific HTTP implementations but you have to re-added some checking logic here. – Alex Sep 22 '17 at 19:08
  • 1
    For decoupling the logic should be moved from interceptors to services. Interceptors are supposed to provide *global* behaviour. If it isn't supposed to be global, this isn't a use case for interceptors. Should it be global indeed if only 2 services demand it? If you believe it should, I'd suggest to open an issue in A4 repo with your case. – Estus Flask Sep 22 '17 at 21:02
  • Any way, you can create a new module with another HttpClient implementation that will differ from default one, as it was mentioned in the answer. It will have its own interceptors. That's the way if you need *decoupled* HttpClient logic. – Estus Flask Sep 22 '17 at 21:06
  • 1
    The 'global' meaning of interceptors significantly reduces their usability. – Alex Che Jun 28 '18 at 13:59
  • @AlexChe Global behaviour is the use case for interceptors. If you don't need to apply them globally, you likely don't need them at all. If you have a problem similar to this question, another option is to have different HttpClient instances, one of which has an interceptor and another one doesn't. This may be tricky because Angular doesn't support this out of the box but certainly possible. – Estus Flask Jun 28 '18 at 14:42
0

Just in case anyone has this issue in the future, this is what I ended up doing.

I needed to add STATIC headers to an HttpRequest but this solution can be easily modified to allow more dynamic headers.

I made an Attribute Adding Interceptor with the following:

attribute.interceptor.ts

@Injectable()
export class AttributesInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {

    const attributes = environment.attributes;
    let attributeReq = req;
    for (let i = 0; i !== attributes.length; i++) {
      const attribute = attributes[i];
      if (this.urlMatches(attribute.urls, req)) {
        // The url matches so let's apply the attribute
        attributeReq = attributeReq.clone({ headers: req.headers.set(attribute.header, attribute.value) });
      }
    }

    return next.handle(attributeReq);
  }

  urlMatches(urls: string[], req: HttpRequest<any>): boolean {
    for (let i = 0; i !== urls.length; i++) {
      if (req.url.includes(urls[i])) {
        return true;
      }
    }

    return false;
  }
}

And defined my static attributes in the environment file:

environment.local.ts

export const endpoints = {
  midway: 'http://localhost:8080',
  builds: 'http://localhost:7245'
};

export const environment = {
  production: false,
  logLevel: LogSeverity.INFO,
  endpoints: endpoints,
  attributes: <Attribute[]> [
    {
      header: 'X-COMPANY',
      value: 'SECRET-TOKEN',
      urls: [ endpoints.midway ]
    }
  ]
};

Where an Attribute is defined as:

export class Attribute {
  header: string;
  value: string;
  urls: string[];
}

So now I can add specific static headers to any service call and just append the this attribute array with the service base url and the header / value.

This solution can be extended to serve a lot more complicated use cases but this simple solutions does exactly what I need!

Alex
  • 422
  • 4
  • 12