Lesson 10-Axios Source Code Analysis

Axios is a Promise-based HTTP client for browsers and Node.js, offering a simple API for handling HTTP requests. Analyzing its source code provides insights into its inner workings, enabling better usage and optimization. Below is a detailed breakdown of Axios’s source code, covering its core functionality and implementation.

Source Code Structure Overview

Axios’s source code structure is organized as follows:

axios/

├── lib/
   ├── axios.js
   ├── defaults.js
   ├── utils.js
   ├── core/
      ├── Axios.js
      ├── dispatchRequest.js
      ├── InterceptorManager.js
      ├── settle.js
      ├── buildFullPath.js
      ├── transformData.js
      └── enhanceError.js
   ├── cancel/
      ├── Cancel.js
      ├── CancelToken.js
      └── isCancel.js
   ├── adapters/
      ├── xhr.js
      └── http.js
   ├── helpers/
      ├── buildURL.js
      ├── combineURLs.js
      ├── isURLSameOrigin.js
      ├── cookies.js
      ├── parseHeaders.js
      ├── spread.js
      └── isAbsoluteURL.js
   └── ...
├── dist/
├── examples/
├── test/
└── index.js

Axios Core Functionality

Creating Instances

Axios allows creating instances with custom configurations, useful for handling different API services.

const axios = require('axios');

const instance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

instance.get('/user')
  .then(response => console.log(response))
  .catch(error => console.error(error));

Request Interceptors

Request interceptors enable modifying requests before they are sent.

const axios = require('axios');

axios.interceptors.request.use(
  config => {
    config.headers.Authorization = 'Bearer token';
    return config;
  },
  error => Promise.reject(error)
);

Response Interceptors

Response interceptors allow processing responses before they reach then or catch.

const axios = require('axios');

axios.interceptors.response.use(
  response => response.data,
  error => Promise.reject(error)
);

Request Configuration

Axios provides various configuration options to customize requests.

const axios = require('axios');

axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
}).then(response => console.log(response));

Request Methods

Axios offers shortcut methods for sending HTTP requests.

const axios = require('axios');

// GET request
axios.get('/user')
  .then(response => console.log(response));

// POST request
axios.post('/user', {
  firstName: 'Fred',
  lastName: 'Flintstone'
}).then(response => console.log(response));

// Concurrent requests
axios.all([
  axios.get('/user'),
  axios.get('/posts')
]).then(axios.spread((userResponse, postsResponse) => {
  console.log(userResponse);
  console.log(postsResponse);
}));

Axios Implementation Details

Axios Class

The Axios class is the core of Axios, encapsulating request and response handling logic.

lib/core/Axios.js:

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  config.method = config.method ? config.method.toLowerCase() : 'get';

  const chain = [dispatchRequest, undefined];
  let promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

// Shortcut methods for HTTP methods
['delete', 'get', 'head', 'options'].forEach(function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url
    }));
  };
});

['post', 'put', 'patch'].forEach(function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

InterceptorManager Class

The InterceptorManager class manages request and response interceptors.

lib/core/InterceptorManager.js:

function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  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) {
  this.handlers.forEach(function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

Request Configuration

Request configurations combine user-provided settings with defaults.

lib/core/mergeConfig.js:

function mergeConfig(config1, config2) {
  config2 = config2 || {};
  const config = {};

  const mergeDeepProperties = ['headers', 'auth', 'proxy', 'params'];
  const valueFromConfig2Keys = ['url', 'method', 'params', 'data'];
  const defaultToConfig2Keys = ['baseURL', 'timeout', 'timeoutErrorMessage', 'withCredentials'];

  mergeDeepProperties.forEach(function deepMerge(prop) {
    if (typeof config2[prop] !== 'undefined') {
      config[prop] = utils.deepMerge(config1[prop], config2[prop]);
    } else if (typeof config1[prop] !== 'undefined') {
      config[prop] = utils.deepMerge(config1[prop]);
    }
  });

  valueFromConfig2Keys.forEach(function valueFromConfig2(prop) {
    if (typeof config2[prop] !== 'undefined') {
      config[prop] = config2[prop];
    }
  });

  defaultToConfig2Keys.forEach(function defaultToConfig2(prop) {
    if (typeof config2[prop] !== 'undefined') {
      config[prop] = config2[prop];
    } else if (typeof config1[prop] !== 'undefined') {
      config[prop] = config1[prop];
    }
  });

  const axiosKeys = mergeDeepProperties.concat(valueFromConfig2Keys).concat(defaultToConfig2Keys);
  const otherKeys = Object.keys(config2).concat(Object.keys(config1)).filter(function filterAxiosKeys(key) {
    return axiosKeys.indexOf(key) === -1;
  });

  otherKeys.forEach(function otherConfig(prop) {
    if (typeof config2[prop] !== 'undefined') {
      config[prop] = config2[prop];
    } else if (typeof config1[prop] !== 'undefined') {
      config[prop] = config1[prop];
    }
  });

  return config;
}

Request Dispatching

The dispatchRequest function handles request sending.

lib/core/dispatchRequest.js:

function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  config.headers = config.headers || {};

  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'].forEach(function cleanHeaderConfig(method) {
    delete config.headers[method];
  });

  const adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
}

Request and Response Interceptors

Interceptors are executed before requests are sent and before responses are processed.

lib/core/Axios.js:

Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  config.method = config.method ? config.method.toLowerCase() : 'get';

  const chain = [dispatchRequest, undefined];
  let promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

Axios Advanced Features

Canceling Requests

Axios supports request cancellation via CancelToken.

lib/cancel/CancelToken.js:

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  let resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  const token = this;
  executor(function cancel(message) {
    if (token.reason) {
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

Using CancelToken to Cancel Requests:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});

// Cancel the request
cancel();

Concurrent Requests

Axios supports concurrent requests using axios.all and axios.spread.

axios.all([
  axios.get('/user'),
  axios.get('/posts')
]).then(axios.spread((userResponse, postsResponse) => {
  console.log(userResponse);
  console.log(postsResponse);
}));

Custom Instances

Create custom instances with axios.create for tailored configurations.

const instance = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 1000,
  headers: {'X-Custom-Header': 'foobar'}
});

instance.get('/user')
  .then(response => console.log(response))
  .catch(error => console.error(error));

Error Handling

Axios provides robust error handling for various stages, including requests, responses, and cancellations.

axios.get('/user/12345')
  .then(response => console.log(response))
  .catch(error => {
    if (error.response) {
      // Server responded with a status outside 2xx
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // Request made but no response received
      console.log(error.request);
    } else {
      // Error during request setup
      console.log('Error', error.message);
    }
  });

Summary

Understanding Axios’s source code reveals its operational principles, empowering developers to leverage its capabilities fully. By exploring its core functionality, implementation details, and advanced features, we can write more efficient and robust HTTP request code.

Share your love