Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Matthew Senn and Michael Senn and Phillip Senn
Ben Nadel at CFinNC 2009 (Raleigh, North Carolina) with: Matthew Senn , Michael Senn , and Phillip Senn@PhillipSenn )

HTTP Interceptors Are An Anti-Pattern That Create Hidden Dependencies And Unnecessary Complexity In Angular

By Ben Nadel on

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.


 
 
 

 
HTTP Interceptors are an anti-pattern that should be avoided in an Angular application.  
 
 
 

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.



Reader Comments

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?

Reply to this Comment

@Charles,

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.

Reply to this Comment

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

Reply to this Comment

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?

Reply to this Comment

@Charles,

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 HttpClient but adds any API-specific headers that would be needed. To this in action, take a look at my previous post:

https://www.bennadel.com/blog/3588-reporting-the-user-s-timezone-offset-to-the-server-using-an-api-client-in-angular-7-2-10.htm

... essentially, it created a small wrapper that injected a X-Timezone-Offset header into header into each request:

export class ApiHttpClient {

	private httpClient: HttpClient;

	constructor( httpClient: HttpClient ) {

		this.httpClient = httpClient;

	}

	// ---
	// PUBLIC METHODS.
	// ---

	// I make an HTTP request to the API and return an observable response.
	public makeRequest<T>( requestConfig: RequestConfig ) : Observable<T> {

		var headers: StringMap = {
			...requestConfig.headers,

			// Pass the timezone offset as a special HTTP header. This way, the server
			// can record this value if it has been changed (based on the user's locale).
			"X-Timezone-Offset": this.getTimezoneOffset()
		};

		var httpStream = this.httpClient.request<T>(
			requestConfig.method,
			requestConfig.url,
			{
				responseType: "json",
				headers: headers
			}
		);

		return( httpStream );

	}

	// ---
	// PRIVATE METHODS.
	// ---

	private getTimezoneOffset() : string {

		return( String( new Date().getTimezoneOffset() ) );

	}

}

Here, you can see that my ApiHttpClient wraps the HttpClient and then merges-in the HTTP Header on every request.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.