HTTP Interceptors Are An Anti-Pattern That Create Hidden Dependencies And Unnecessary Complexity In Angular
CAUTION: What follows is 100% my subjective opinion.
This morning, in my post about reporting a user's Timezone with a custom HTTP header in an Angular application, one of my readers asked me why I was creating a custom HTTP Client instead of simply using an HTTP Interceptor to augment the outgoing HTTP headers collection. My answer to that is simple: I believe that HTTP Interceptors represent an anti-pattern in my Angular applications. I believe that they create hidden dependencies, unnecessary complexity, and subtle bugs, all of which can be alleviated with the use of specialized HTTP Clients.
To be clear, I didn't always feel this way. In fact, in my AngularJS days, I used HTTP Interceptors with some success. However, over time, as the applications grew and the engineering teams evolved, I began to see the drawbacks of HTTP Interceptors outweigh the benefits that we were experiencing during the early phases of the application development life-cycle.
HTTP Interceptors scatter the configuration of HTTP requests.
Using an HTTP Interceptor to manipulate HTTP requests and responses make every HTTP request harder to reason about because the configuration of each HTTP workflow is scattered across the application file system in ways that are not obvious. If a particular HTTP request is behaving incorrectly, the debugging process involves finding the inception of the HTTP request and then tracing the method-calls. However, there is nothing in any of the method-calls - not even in the service that makes the HTTP request - to indicate that an HTTP Interceptor is altering the HTTP processing logic. If there was a single source of truth for the HTTP request wrangling, debugging would be simple.
HTTP Interceptors require a lot of "tribal knowledge".
Because the HTTP Interceptor scatters HTTP request configuration logic across the file system, it requires a lot of "tribal knowledge" in order to understand and maintain these workflows. This means that as your engineering team begins to churn over time, new engineers will have to ramp-up on this "tribal knowledge" before they can become effective at building and maintaining the Angular application.
HTTP Interceptors create hidden dependencies.
Since all HTTP requests in a given application use the same HTTP Interceptors, these HTTP Interceptors create hidden dependencies. For example, trying to add a small delay to an AJAX request may accidentally slow-down the fetching of HTML templates. This kind of unintended consequence is caused by the unseen relationship between the disparate parts of the application. For me, this is a clear violation of the "Principle of Least Surprise"; and, once you run into this issue, it can become frightening to change any of the HTTP interceptors lest you break something you didn't even know would be affected.
HTTP Interceptors create false DRY'ness
In an effort to create a DRY (Don't Repeat Yourself) application, it can be tempting to see HTTP Interceptors as a way to factor-out the shared properties across all HTTP requests. But, this is based on the incorrect assumption that all HTTP requests in a given application share some set of cohesive behaviors. While this may be true on day-one, it will very likely become a poor assumption over time. And, as you application continues to evolve, this false DRY'ness becomes a point-of-friction, not an opportunity for efficiency.
HTTP Interceptors create false assumptions.
By creating an HTTP Interceptor, you may assume that all HTTP requests being triggered by the application are affected by your HTTP Interceptor. But, this is only true for HTTP requests that use the HttpClient module; and, only in cases where the Dependency-Injector uses the same HTTP Interceptor provider. The moment you have an engineer that wants to use Axios or some "Fetch" API to build her remote-gateway, your application is no longer operating under the same assumptions.
HTTP Interceptors are basically shared, global state.
Really, all of these problems stem from the fact that HTTP Interceptors are, essentially, shared global state. As a community, we've come to understand that shared global state is a "Bad Thing" (tm) and leads to tightly-coupled, brittle code that grows harder and harder to maintain over time. If you move all of the HTTP request configuration into specialized services, you can break that shared state up into isolated silos that can evolve independently of each other.
HTTP Interceptors add unnecessary complexity.
On top of the architectural and maintenance issues, HTTP Interceptors are just complicated. In AngularJS, they were Promises, which was one thing. But now, in Angular 7, interceptors use Observable HTTP event streams in which configuration data has to be explicitly cloned in order to work. This requires a lot more finagling when compared to the equivalent behavior inside of a specialized HTTP client.
Your mileage may vary. It's possible that for you, in your Angular applications, HTTP Interceptors create huge efficiencies. In my experience, however, HTTP Interceptors become a maintenance bottleneck that creates tightly-coupled, brittle code that is harder to reason about. By moving the same behavior into specialized HTTP Clients, I find that all HTTP workflows become more predictable and easier to evolve over time.
This is interesting. I have never really thought too much about the implications of using an interceptor. But, I may use your check list, in future, to see, if any of your scenarios apply.
At the moment, I am only using an interceptor to send a 'user token' & 'JWE token' to the server for authorization purposes. So, I guess this is OK, because it is a fairly straightforward behavior. And, I only ever use the HttpClient module.
I just find it, a bit annoying, having to add these 2 properties to every individual http request, so an interceptor seems like a good fit here. I can also see whether it is being sent to the server, in my request, by using Fiddler.
I have to say I did wonder, the other day, about the way the interceptor clones the request. I wonder whether this is an expensive operation?
Keep in mind, again, this is purely my opinion on the matter -- clearly people do use Http Interceptors with success (including myself). This is just some conclusions that I've drawn over time based on my own experience.
Re: adding those headers to every request - that's exactly why I like creating custom Http Clients - you can create an internal method for the HTTP call that does that for you. So, it's more or less like the Interceptor, except the interceptor is a "private method call" rather than some function you provide to the Angular Dependency-Injector.
This corresponds with my own experience. Things like "adding those headers to every request" are pretty trivial when you do it yourself. All you need is to wrap the provided service (or Fetch or XMLHttpRequest or whatever) in some code of your own and never use the wrapped thing directly.
Interceptors should IMHO only be used, when effects like "slow-down the fetching of HTML templates" are intentional (e.g., for testing).
Ben. That's interesting. How would you add a custom HTTP Interceptor to every request? I'm not sure how that would work? Do you have to pass the interceptor on to the main HTTP request via a callback or something?
It would be just like @Maaartinus was saying -- you just need to create a thin Service that wraps calls to the underlying
HttpClient. Then, you use the thin service, which in turn, invokes the
HttpClientbut adds any API-specific headers that would be needed. To this in action, take a look at my previous post:
... essentially, it created a small wrapper that injected a
X-Timezone-Offsetheader into header into each request:
Here, you can see that my
HttpClientand then merges-in the HTTP Header on every request.
Isn't another concern that, lets say my app authenticates with and then consumes API_1. I then use an interceptor to tack on the jwt or whatever auth that API_1 requires.
Later my app starts consuming API_2, from a 3rd party. API_2 doesn't share the same authentication mechanism as API_1. My interceptor is now presumably tacking my jwt or whatever from API_1 to requests to API_2 which is obviously a security risk.
Now I could go and edit my interceptor to look at the target URL and then add the auth or not (or add different auth). But the interceptor will just become a jumbled mess of if/elses.
That's a really great point. Ultimately, each API is going to have different requirements. If you try to code that all via HTTP Interceptors, you paint yourself into a corner. By creating an API Client, you define a clean separation of concerns and let each Client evolve independently as needed.
Awesome article, it would be really nice to have your insight on the following points:
In a recent interview I was asked if I'll add jwt tokens in every request using interceptors wouldn't it have a negative impact on performance.
www.bennadel.com/blog/3047-creating-specialized-http-clients-in-angular-2-beta-8.htm , will this post still fit in with Angular 9-10, your thoughts on this.
Awesome article, it would be really nice to have your insight on the following points:
2)( www.bennadel.com/blog/3047-creating-specialized-http-clients-in-angular-2-beta-8.htm) , will this post still fit in with Angular 9-10, your thoughts on this.