This is the first article in a series of source code pickups. After reading this article, the following questions should be answered.
- What is the Axios adapter principle?
- How does Axios implement request and response interception?
- How does Axios cancel requests work?
- How does CSRF work? How does Axios defend against client-side CSRF attacks?
- How is request and response data conversion implemented?
The full text is about 2,000 words and takes about 6 minutes to read. The Axios version of the article is 0.21.1
Using features as an entry point, we’ll answer the above questions while taking a look at the art of minimalism in Axios source code.
Features
- Create an XMLHttpRequest from the browser
- Create HTTP requests from Node.js
- Supporting Promise API
- Intercept requests and responses
- Cancel the request
- Automatically load and replace JSON data
- Supports client XSRF attacks
The first two features explain why Axios can be used in both browsers and Node.js, simply by deciding whether to use XMLHttpRequest or Node.js HTTP to create requests, This compatible logic is called an adapter, and the corresponding source is in lib/defaults.js,
// defaults.js
function getDefaultAdapter() {
var adapter;
if (typeofXMLHttpRequest ! = ='undefined') {
// For browsers use XHR adapter
adapter = require('./adapters/xhr');
} else if (typeofprocess ! = ='undefined' && Object.prototype.toString.call(process) === '[object process]') {
// For node use HTTP adapter
adapter = require('./adapters/http');
}
return adapter;
}
Copy the code
This is the adapter’s judgment logic, which determines which adapter to use by detecting some global variables of the current environment. The judgment logic of Node environment can also be reused when we do SSR server rendering. Let’s take a look at Axios wrapping the adapter.
Adapter xhr
Locate the source file lib/ Adapters /xhr.js and take a look at the overall structure.
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
// ...})}Copy the code
Exports a function that takes a configuration parameter and returns a Promise. We pull out the key parts,
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var request = new XMLHttpRequest();
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
request.onreadystatechange = function handleLoad() {}
request.onabort = function handleAbort() {}
request.onerror = function handleError() {}
request.ontimeout = function handleTimeout() {}
request.send(requestData);
});
};
Copy the code
Does it feel familiar? Yes, that’s how XMLHttpRequest is used: you create an XHR and then open starts the request, listens for the XHR status, and then send sends the request. So let’s expand on what Axios does to onReadyStatechange,
request.onreadystatechange = function handleLoad() {
if(! request || request.readyState ! = =4) {
return;
}
// The request errored out and we didn't get a response, this will be
// handled by onerror instead
// With one exception: request that using file: protocol, most browsers
// will return status as 0 even though it's a successful request
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') = = =0)) {
return;
}
// Prepare the response
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
varresponseData = ! config.responseType || config.responseType ==='text' ? request.responseText : request.response;
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
settle(resolve, reject, response);
// Clean up request
request = null;
};
Copy the code
The state is filtered first and only processed when the request completes (readyState === 4). Note that if an XMLHttpRequest request fails, onerror can be heard in most cases, with one exception: when the request uses the file protocol (file://), most browsers will return a status code of 0 even if the request succeeds.
Axios handles this exception as well.
Once the request is complete, it’s time to process the response. This wraps the response as a standard-format object, passed as a third argument to the settle method, which is defined in lib/core/settle.
function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
if(! response.status || ! validateStatus || validateStatus(response.status)) { resolve(response); }else {
reject(createError(
'Request failed with status code ' + response.status,
response.config,
null, response.request, response )); }};Copy the code
Settle simply wraps Promise’s callback, ensuring that the call returns in a certain format.
This is the main logic of xhrAdapter, the rest is the request header, some supported configuration items and the simple handling of callbacks such as timeout, error, cancel request, etc. The defense against XSRF attack is realized through the request header.
Let’s start with a quick review of what XSRF (also known as CSRF, cross-site request forgery) is.
CSRF
Background: After a user logs in, the user needs to store the login credentials and keep the login state, instead of sending the account password for each request.
How do I stay logged in?
At present, a common way is that the server adds set-cookie option in the response header after receiving an HTTP request and stores the credentials in the Cookie. After receiving the response, the browser stores cookies. According to the same origin policy of the browser, the next time it sends a request to the server, Cookies are automatically carried along with server authentication to keep the user logged in.
Therefore, if we do not judge the legitimacy of the request source and send a forged request to the server through other websites after login, the Cookie carrying the login credentials will be sent to the server along with the forged request, resulting in security vulnerability, which is what we call CSRF, cross-site request forgery.
Therefore, the key to preventing forged requests is to check the source of the request. Although the Refferer field can identify the current site, it is not reliable enough. Currently, the common solution in the industry is to attach an anti-CSRF token to each request. So we can determine whether the request is forged by encrypting the Cookie (for example, the SID) and doing some simple authentication with the server.
Axios simply implements support for special CSRF tokens,
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
// Add xsrf header
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if(xsrfValue) { requestHeaders[config.xsrfHeaderName] = xsrfValue; }}Copy the code
Interceptor
Interceptors are a Feature of Axios. Let’s briefly review how to use them.
// Interceptors can intercept requests or responses
// Interceptor callback will be invoked before request or response then or catch callback
var instance = axios.create(options);
var requestInterceptor = axios.interceptors.request.use(
(config) = > {
// do something before request is sent
return config;
},
(err) = > {
// do somthing with request error
return Promise.reject(err); });// Remove the set interceptor
axios.interceptors.request.eject(requestInterceptor)
Copy the code
So how does the interceptor work?
Locate the source code at line 14 of lib/core/ axios.js,
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
Copy the code
As you can see from the Axios constructor, both request and Response in the interceptors are instances called InterceptorManager. What is the InterceptorManager?
Locate the source lib/core/InterceptorManager js,
function InterceptorManager() {
this.handlers = [];
}
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected,
synchronous: options ? options.synchronous : false.runWhen: options ? options.runWhen : null
});
return this.handlers.length - 1;
};
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null; }}; InterceptorManager.prototype.forEach =function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if(h ! = =null) { fn(h); }}); };Copy the code
InterceptorManager is a simple event manager that manages interceptors.
Interceptors are stored through handlers, which then provide methods to add, remove, and iterate through instances of interceptors. Each interceptor object stored contains callbacks to resolve and Reject as promises, as well as two configuration items.
It is worth noting that the removal method is implemented by directly setting the interceptor object to NULL, rather than the splice clipping array, and the corresponding NULL value handling is added to the traversal method. This allows each item ID to remain the same as the item’s array index and avoids the performance penalty of re-splicing the array.
Interceptor callbacks are called before then or catch callbacks for requests or responses. How is this implemented?
Back at line 27 in the source code lib/core/ axios.js, the request method on the Axios instance object,
The key logic we extracted is as follows,
Axios.prototype.request = function request(config) {
// Get merged config
// Set config.method
// ...
var requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
var promise;
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
};
Copy the code
As you can see, when a request is executed, the actual request (dispatchRequest) and interceptor are managed through a queue called chain. The logic of the entire request is as follows,
- The request and response interceptor queues are initialized, and the resolve and reject callbacks are placed at the head of the queue
- A Promise is then initialized to perform the callback and the chain is used to store and manage the actual request and interceptor
- The request interceptor is placed at the head of the chain team and the response interceptor is placed at the tail of the chain team
- When the queue is not empty, the request interceptor, the actual request, and the response interceptor are dequeued through a chain call to promise. then
- Finally, return the Promise after the chain call
The actual request here is the encapsulation of the adapter, and the transformation of the request and response data is done here.
So how does the data transformation work?
Transform data
Locate the source lib/core/dispatchRequest js,
function dispatchRequest(config) {
throwIfCancellationRequested(config);
// Transform request data
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// Transform response data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
return response;
}, function onAdapterRejection(reason) {
if(! isCancel(reason)) { throwIfCancellationRequested(config);// Transform response data
if(reason && reason.response) { reason.response.data = transformData( reason.response.data, reason.response.headers, config.transformResponse ); }}return Promise.reject(reason);
});
};
Copy the code
ThrowIfCancellationRequested method is used to cancel the request, here we will discuss later about cancellation request, you can see the requests are implemented by calling the adapter, before the call and call on the request and response data conversion.
Conversion is implemented using the transformData function, which iterates through the conversion function that we set up to call. The conversion function takes headers as a second argument, so we can perform some different conversion operations based on the information in headers.
/ / source core/transformData. Js
function transformData(data, headers, fns) {
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});
return data;
};
Copy the code
Axios also provides two default conversion functions for converting request and response data. By default,
Axios does something with the data that comes in for the request, like serialize the request data as a JSON string if it’s an object, and try to convert the response data into a JavaScript object if it’s a JSON string, which is very useful.
The corresponding converter source can be found at line 31 of lib/default.js,
var defaults = {
// Line 31
transformRequest: [function transformRequest(data, headers) {
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded; charset=utf-8');
return data.toString();
}
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json; charset=utf-8');
return JSON.stringify(data);
}
returndata; }].transformResponse: [function transformResponse(data) {
var result = data;
if (utils.isString(result) && result.length) {
try {
result = JSON.parse(result);
} catch (e) { /* Ignore */}}returnresult; }}],Copy the code
We said Axios supports cancellation requests. How?
CancelToken
Both XHR and Node.js HTTP request objects provide abort to cancel requests, so call abort when appropriate.
So what is the right time? It is appropriate to give control to the user. So it’s up to the user to decide when to cancel the request, which means we need to expose the cancellation method. Axios cancels the request by CancelToken.
First, Axios provides two ways to create the Cancel token,
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
CancelToken (CancelToken) CancelToken (CancelToken
axios.post("/user/12345", { name: "monch" }, { cancelToken: source.token });
source.cancel();
// The CancelToken constructor is instantiated by itself
let cancel;
axios.post(
"/user/12345",
{ name: "monch" },
{
cancelToken: new CancelToken(function executor(c) { cancel = c; })}); cancel();Copy the code
What exactly is CancelToken? Locate the source lib/cancel/CancelToken js line 11,
function CancelToken(executor) {
if (typeofexecutor ! = ="function") {
throw new TypeError("executor must be a function.");
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
Copy the code
CancelToken is a minimalist state machine controlled by promises. When instantiated, a promise is mounted on the instance. The promise’s resolve callback is exposed to the external method Executor. This is a big pity. When we call this executor method externally, we will get a fulfilled promise. How can we cancel the request once we have this promise?
Should we just get the promise instance on the request and cancel the request in the then callback?
Locate the adapter source lib/ Adapters /xhr.js on line 158,
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if(! request) {return;
}
request.abort();
reject(cancel);
// Clean up request
request = null;
});
}
Copy the code
And line 291 of lib/adaptors/http.js,
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (req.aborted) return;
req.abort();
reject(cancel);
});
}
Copy the code
Sure enough, the XHR or HTTP. request abort method is called in the adapter’s THEN callback to the Promise of the CancelToken instance. Consider that if we did not call the CancelToken method externally, this would mean that the resolve callback would not be executed, and the promise’s then callback in the adapter would not be executed, so abort would not be called.
summary
Axios encapsulates the adapter so that it can be used in both browsers and Node.js while maintaining the same set of interface specifications. Source code uses a lot of Promise and closure features, to achieve a series of state control, which for interceptor, cancel request implementation embodies its minimalist encapsulation art, worth learning and reference.
Refer to the link
- Axios Docs – axios-http.com
- Axios Github Source Code
- Source code glean Axios – the art of minimal packaging
- Cross Site Request Forgery – Part III. Web Application Security
- tc39/proposal-cancelable-promises
Write in the last
This article first in my blog, uneducated, unavoidably have mistakes, the article is wrong also hope not hesitate to correct!
If you have any questions or find errors, you can ask questions or correct errors in the corresponding issues
If you like or are inspired by it, welcome star and encourage the author
(after)