Interceptor Design and Implementation
Interceptor Design and Implementation
# Requirements Analysis
We want to be able to intercept request sending and responses -- that is, to perform some additional logic before sending requests and after receiving responses.
The desired usage of the interceptors is as follows:
// Add a request interceptor
axios.interceptors.request.use(function (config) {
// Do something before sending the request
return config;
}, function (error) {
// Handle request error
return Promise.reject(error);
});
// Add a response interceptor
axios.interceptors.response.use(function (response) {
// Process response data
return response;
}, function (error) {
// Handle response error
return Promise.reject(error);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
On the axios object there is an interceptors property, which in turn has request and response properties. Both have a use method that accepts 2 parameters: the first is similar to a Promise's resolve function, and the second is similar to a Promise's reject function. We can execute synchronous or asynchronous code logic in the resolve and reject functions.
We can add multiple interceptors, and they are executed in a chained sequential manner. For request interceptors, later-added interceptors execute first in the pre-request process; for response interceptors, earlier-added interceptors execute first after the response.
axios.interceptors.request.use(config => {
config.headers.test += '1'
return config
})
axios.interceptors.request.use(config => {
config.headers.test += '2'
return config
})
2
3
4
5
6
7
8
Additionally, we can support removing a specific interceptor, as follows:
const myInterceptor = axios.interceptors.request.use(function () {/*...*/})
axios.interceptors.request.eject(myInterceptor)
2
# Overall Design
Let's first use a diagram to illustrate the interceptor workflow:

The entire process follows a chained call pattern, and each interceptor can support both synchronous and asynchronous processing. This naturally leads us to use Promise chaining to implement the entire call process.
During the execution of this Promise chain, request interceptor resolve functions process the config object, while response interceptor resolve functions process the response object.
After understanding the interceptor workflow, we first need to create an interceptor manager class that allows us to add, remove, and traverse interceptors.
# Interceptor Manager Class Implementation
Based on the requirements, axios has an interceptors property, which in turn has request and response properties that expose a use method to add interceptors. We can think of these two properties as interceptor manager objects. The use method accepts 2 parameters: the first is a resolve function and the second is a reject function. For the resolve function's parameter, request interceptors use the AxiosRequestConfig type while response interceptors use the AxiosResponse type; for the reject function's parameter, the type is any.
Based on the above analysis, let's first define the external interface for the interceptor manager object.
# Interface Definition
types/index.ts:
export interface AxiosInterceptorManager<T> {
use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number
eject(id: number): void
}
export interface ResolvedFn<T=any> {
(val: T): T | Promise<T>
}
export interface RejectedFn {
(error: any): any
}
2
3
4
5
6
7
8
9
10
11
12
13
Here we define the AxiosInterceptorManager generic interface, because the resolve function parameters differ between request interceptors and response interceptors.
# Code Implementation
import { ResolvedFn, RejectedFn } from '../types'
interface Interceptor<T> {
resolved: ResolvedFn<T>
rejected?: RejectedFn
}
export default class InterceptorManager<T> {
private interceptors: Array<Interceptor<T> | null>
constructor() {
this.interceptors = []
}
use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number {
this.interceptors.push({
resolved,
rejected
})
return this.interceptors.length - 1
}
forEach(fn: (interceptor: Interceptor<T>) => void): void {
this.interceptors.forEach(interceptor => {
if (interceptor !== null) {
fn(interceptor)
}
})
}
eject(id: number): void {
if (this.interceptors[id]) {
this.interceptors[id] = null
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
We defined a generic class InterceptorManager that internally maintains a private property interceptors, an array used to store interceptors. The class exposes 3 methods: the use method adds an interceptor to interceptors and returns an id for deletion; the forEach method is for traversing interceptors -- it accepts a function and calls that function during traversal, passing each interceptor as an argument; eject removes an interceptor by its id.
# Chain Call Implementation
This section requires understanding of Promises. You can learn more at mdn (opens new window).
After implementing the interceptor manager class, the next step is to define an interceptors property in Axios with the following type:
interface Interceptors {
request: InterceptorManager<AxiosRequestConfig>
response: InterceptorManager<AxiosResponse>
}
export default class Axios {
interceptors: Interceptors
constructor() {
this.interceptors = {
request: new InterceptorManager<AxiosRequestConfig>(),
response: new InterceptorManager<AxiosResponse>()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
The Interceptors type has 2 properties: a request interceptor manager class instance and a response interceptor manager class instance. When instantiating the Axios class, we initialize this interceptors instance property in the constructor.
Next, we modify the request method logic to add the interceptor chain call logic:
core/Axios.ts:
interface PromiseChain {
resolved: ResolvedFn | ((config: AxiosRequestConfig) => AxiosPromise)
rejected?: RejectedFn
}
request(url: any, config?: any): AxiosPromise {
if (typeof url === 'string') {
if (!config) {
config = {}
}
config.url = url
} else {
config = url
}
const chain: PromiseChain[] = [{
resolved: dispatchRequest,
rejected: undefined
}]
this.interceptors.request.forEach(interceptor => {
chain.unshift(interceptor)
})
this.interceptors.response.forEach(interceptor => {
chain.push(interceptor)
})
let promise = Promise.resolve(config)
while (chain.length) {
const { resolved, rejected } = chain.shift()!
promise = promise.then(resolved, rejected)
}
return promise
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
First, we construct an array chain of type PromiseChain and assign the dispatchRequest function to the resolved property. Then we traverse the request interceptors and insert them at the front of chain; then we traverse the response interceptors and append them to the end of chain.
Next, we define an already-resolved promise, loop through chain, get each interceptor object, and add their resolved and rejected functions as arguments to promise.then. This effectively implements the layered chain call effect of interceptors through Promise chaining.
Note the execution order of interceptors: for request interceptors, later-added ones execute first, then earlier-added ones; for response interceptors, earlier-added ones execute first, then later-added ones.
# Writing the Demo
Create an interceptor directory under the examples directory, and create index.html inside the interceptor directory:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Interceptor example</title>
</head>
<body>
<script src="/__build__/interceptor.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
Then create app.ts as the entry file:
import axios from '../../src/index'
axios.interceptors.request.use(config => {
config.headers.test += '1'
return config
})
axios.interceptors.request.use(config => {
config.headers.test += '2'
return config
})
axios.interceptors.request.use(config => {
config.headers.test += '3'
return config
})
axios.interceptors.response.use(res => {
res.data += '1'
return res
})
let interceptor = axios.interceptors.response.use(res => {
res.data += '2'
return res
})
axios.interceptors.response.use(res => {
res.data += '3'
return res
})
axios.interceptors.response.eject(interceptor)
axios({
url: '/interceptor/get',
method: 'get',
headers: {
test: ''
}
}).then((res) => {
console.log(res.data)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
In this demo, we added 3 request interceptors and 3 response interceptors, then removed the second response interceptor. When running this demo in the browser, the request we send has a test request header with the value 321; the response data returned is hello, and after processing by the response interceptors, the final output is hello13.
At this point, we have implemented the interceptor feature for ts-axios. It is a very practical feature that can be used in real-world scenarios such as login authentication.
Currently, when sending requests through axios, we often pass in a bunch of configuration. However, we also want ts-axios itself to have some default configuration, and we merge the user's custom configuration with the defaults. In fact, most JS libraries work in a similar way. In the next chapter, we will implement this feature.