axios 中是如何处理异常的?

1,917 阅读10分钟

本文是“axios源码系列”第七篇,你可以查看以下链接了解过去的内容。

  1. axios 是如何实现取消请求的?
  2. 你知道吗?axios 请求是 JSON 响应优先的
  3. axios 跨端架构是如何实现的?
  4. axios 拦截器机制是如何实现的?
  5. axios 浏览器端请求是如何实现的?
  6. axios 对外出口API是如何设计的?

本文我们将讨论 axios 中是如何处理异常的,在此之前,我们先了解以下 axios 中各种类型的异常。

axios 中的正常请求

axios 中当请求服务正常返回时,会落入 .then() 方法中。

axios.get('https://httpstat.us/200')
  .then(res => {
    console.log(res)
  })

效果如下:

axios 会把响应结果包装在返回的 Response 对象的 data 属性中,除此之外:

  1. config:即请求配置
  2. headers:响应头数据(AxiosHeaders 对象)
  3. request:请求实例。浏览器环境就是 XMLHttpRequest 对象
  4. status:HTTP 状态码。本案例是 200,表示请求成功处理了
  5. statusText: 状态码的文字说明

axios 中的异常请求

axios 中的异常请求分 2 类:有响应异常请求和无响应的异常请求。

有响应的异常

当返回的 HTTP 状态码是 2xx 之外的是狗,就会进入 axios 的 .catch() 方法中。

403 响应:

axios.get('https://httpstat.us/403')
  .catch(err => {
    console.log(err)
  })

效果:

500 响应:

axios.get('https://httpstat.us/500')
  .catch(err => {
    console.log(err)
  })

效果:

以上 2 个场景,返回的都是一个 AxiosError 对象,它继承自 Error。相比 Error,AxiosError 除了常规的 code、message、name 和 stack 属性(非标准)外,还包含 config、request 和 reponse:

  • response 就是响应对象,与正常请求时返回的响应对象完全一致
  • config 和 request 与 Response 对象里的一样——前者是请求配置,后者则是底层的请求对象

自定义 validateStatus()

当然,对于有响应的请求,2xx 状态码进入 then,之外的状态码进入 catch 是 axios 的默认配置——通过 validateStatus() 设置的。

// `validateStatus` defines whether to resolve or reject the promise for a given
// HTTP response status code. If `validateStatus` returns `true` (or is set to `null`
// or `undefined`), the promise will be resolved; otherwise, the promise will be
// rejected.
validateStatus: function (status) {
  return status >= 200 && status < 300; // default
},

在 axios 内部,当接收到响应后,会将响应码传入 validateStatus() 函数校验。返回 true,就表示请求成功,否则表示请求失败。

你可以自由调整这里的判断,决定哪类响应可以作为成功的请求处理。

比如,将返回状态码 4xx 的请求也看做是成功的。

axios.get('https://httpstat.us/404', {
  validateStatus: function (status) {
    return status < 500; // Resolve only if the status code is less than 500
  }
})
  .then(res => {
    console.log(res)
  })

效果:

我们设置可以将 validateStatus 设置为 null,将所有有响应返回的请求都看作是成功的,这样也能进入 .then() 中处理了。

axios.get('https://httpstat.us/500', {
  validateStatus: null
})
  .then(res => {
    console.log(res)
  })

效果:

无响应的异常

不过某些请求是没有响应返回的。比如:网络中断、跨域错误、超时、取消请求等。这类异常请求都没有响应返回,但都会落入到 .catch() 里。

网络中断

先以网络中断的情况举例。

axios.get('https://httpstat.us/200?sleep=10000', {
})
  .catch(err => {
    console.log(err)
  })

我们模拟了一个耗时 10s 的请求,在此期间,我们将电脑的网络断掉。就能看到效果。

这个时候可以发现,catch() 中接收到 Axios 对象是没有 response 属性的,说明没有服务响应。同时,错误信息是“Network Error”,也就是网络服务。

当然,无效地址以及跨域错误,也报错 “Network Error”

超时报错

再演示一个请求超时的案例。

axios.get('https://httpstat.us/200?sleep=10000', {
  timeout: 1000
})
  .catch(err => {
    console.log(err)
  })

我们模拟了个 10s 返回的请求,而超时限制设置在了 1s。运行代码,效果如下:

显而易见,错误里依然没有 response 属性,错误的消息也很清晰的说明了问题:"过了 1s 的超时限制了"。

取消请求

axios 中还提供了取消请求的方案。

const controller = new AbortController();

axios.get('https://httpstat.us/200?sleep=10000', {
  signal: controller.signal
})
  .catch(err => {
    console.log(err)
  })

controller.abort();

效果如下:

catch() 捕获到的是一个 CanceledError 对象,它继承了 AxiosError,这样我们就能单独判断这类自动取消的情况了。注意,这里依然是没有 response 属性的。

当然,axios 中还有一个旧的取消方案——使用 CancelToken。

axios.get('https://httpstat.us/200?sleep=10000', {
  cancelToken: source.token
})
  .catch(res => {
    console.log(res)
  })

source.cancel();

相比较于 AbortController 触发的取消,少了 config 和 request 属性。

以上,我们就列完了 aioxs 中各类异常请求的场景及表现。官方仓库 README 的 Handling Errors 也对此做了归纳。

axios.get('/user/12345')
  .catch(function (error) {
    if (error.response) {
      // 请求发出,并且得到服务器响应
      // 响应码在 2xx 之外(默认)
      console.log(error.response.data);
      console.log(error.response.status);
      console.log(error.response.headers);
    } else if (error.request) {
      // 请求发出,但没有响应返回
      // `error.request` 对应底层请求对象。浏览器环境是 XMLHttpRequest 实例,Node.js 环境下则是 http.ClientRequest 实例
      console.log(error.request);
    } else {
      // 在请求准备/响应处理阶段出错了
      console.log('Error', error.message);
    }
    console.log(error.config);
  });

接下来就来分析 axios 中是如何实现请求的异常处理的。

源码分析

我们还是以 axios 的浏览器端实现(lib/adapters/xhr.js)为例。

AxiosError

通过前面的学习,我们知道 axios 抛出的异常是基于 Error 基类封装的 AxiosError,其源代码位于 /lib/core/AxiosError.js

function AxiosError(message, code, config, request, response) {
  // 1)
  Error.call(this);
  // 2)
  if (Error.captureStackTrace) {
    Error.captureStackTrace(this, this.constructor);
  } else {
    this.stack = (new Error()).stack;
  }
  // 3)
  this.message = message;
  this.name = 'AxiosError';
  // 4)
  code && (this.code = code);
  config && (this.config = config);
  request && (this.request = request);
  response && (this.response = response);
}

简单做一些说明:

  1. Error.call(this) 的作用类似调用父级构造函数,AxiosError 实例原型也成 Error 实例了
  2. 收集报错栈信息,优先以 Error.captureStackTrace 方式收集,方便排查问题
  3. 设置常规属性 message 和 name
  4. 扩展出 code、code、code 和 response,这些都是可选的

当然 AxiosError 还有其他代码,因为本文不涉及,就不再赘述。

介绍完 AxiosError,就可以分析 axios 中是如何抛出 AxiosError 的了。

XMLHttpRequest 对象

在能够抛出异常之前,我们需要先创建请求对象 request。

// https://github.com/axios/axios/blob/v1.6.8/lib/adapters/xhr.js#L76
let request = new XMLHttpRequest();

浏览器环境,request 就是 XMLHttpRequest 实例,接下来的异常处理都是基于 request 上的监听事件捕获的。

无响应异常的处理

接下来,我们先讲无响应异常的处理,因为它们的相对逻辑比较简单。

网络异常

这类异常包括:网络中断、跨域错误以及请求地址错误。通过监听 request 的 onerror 事件实现:

// /v1.6.8/lib/adapters/xhr.js#L158-L166
// Handle low level network errors
request.onerror = function handleError() {
  // Real errors are hidden from us by the browser
  // onerror should only fire if it's a network error
  reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));

  // Clean up request
  request = null;
};

直接返回了一个 reject 状态的 Promise,表示请求失败。并返回了 CODE 值为 ERR_NETWORK 的 AxiosError 对象。

超时处理

再来看看对超时的处理,监听了 ontimeout 事件。

// /v1.6.8/lib/adapters/xhr.js#L168-L183
// Handle timeout
request.ontimeout = function handleTimeout() {
  let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
  const transitional = config.transitional || transitionalDefaults;
  if (config.timeoutErrorMessage) {
    timeoutErrorMessage = config.timeoutErrorMessage;
  }
  reject(new AxiosError(
    timeoutErrorMessage,
    transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
    config,
    request));

  // Clean up request
  request = null;
};

处理也很简单,同样是 reject Promise,同时抛出一个 CODE 值为 ECONNABORTED 的 AxiosError 对象。

transitional 配置对象是为了向后兼容才保留的,已不再推荐使用,所以你可以忽略这部分你的判断逻辑。

另外,你还可以通过传入 config.timeoutErrorMessage 配置,自定义超时报错消息。

取消请求

取消请求依赖的是监听 onabort 事件。

// /v1.6.8/lib/adapters/xhr.js#L146-L156
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
  if (!request) {
    return;
  }

  reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));

  // Clean up request
  request = null;
};

当你调用 request 上的 abort() 方法时,就会触发这个事件调用。

在 axios 内部,不管你是通过 signal 还是通过 cancelToken(已弃用),内部都是通过调用 request.abort() 来中止请求的。

取消请求的报错 CODE 值跟超时一样也是 ECONNABORTED,不过报错消息是“Request aborted”。这样你就能区分这次请求是浏览器取消的还是人工取消的了。

// /v1.6.8/lib/adapters/xhr.js#L231-L247
if (config.cancelToken || config.signal) {
  // Handle cancellation
  // eslint-disable-next-line func-names
  onCanceled = cancel => {
    if (!request) {
      return;
    }
    reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
    request.abort();
    request = null;
  };

  config.cancelToken && config.cancelToken.subscribe(onCanceled);
  if (config.signal) {
    config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  }
}

再来看看,有响应的异常处理逻辑。

有响应异常的处理

axios 内部通过监听 onloadend 事件来处理有响应的异常请求。

// /v1.6.8/lib/adapters/xhr.js#L125
request.onloadend = onloadend

在《axios 浏览器端请求是如何实现的?》我们已经介绍过,不管当前请求是否成功,onloadend 回调总是会调用,这里其实是可以使用 onload 事件替代的。

request.onload = onloadend

之所以有这部分逻辑是为了向后兼容,因为 axios 中这部分的完整逻辑是这样的。

if ('onloadend' in request) {
  // Use onloadend if available
  request.onloadend = onloadend;
} else {
  // Listen for ready state to emulate onloadend
  request.onreadystatechange = function handleLoad() {
    // ...
    
    // readystate handler is calling before onerror or ontimeout handlers,
    // so we should call onloadend on the next 'tick'
    setTimeout(onloadend);
  }
}

OK,我们继续看 onloadend 函数的内容:

// /v1.6.8/lib/adapters/xhr.js#L92-L121
function onloadend() {
  // 1)
  if (!request) {
    return;
  }
  
  // 2)
  // Prepare the response
  const responseHeaders = AxiosHeaders.from(
    'getAllResponseHeaders' in request && request.getAllResponseHeaders()
  );
  const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
    request.responseText : request.response;
  const response = {
    data: responseData,
    status: request.status,
    statusText: request.statusText,
    headers: responseHeaders,
    config,
    request
  };
  
  // 3)
  settle(function _resolve(value) {
    resolve(value);
    done();
  }, function _reject(err) {
    reject(err);
    done();
  }, response);
  
  // Clean up request
  request = null;
}
  1. 这里做了 request 的非空判断。因为 onloadend 在调用之前,可以已经在其他的回调事件中处理了,直接返回即可
  2. 这里则是准备返回的响应数据。先收集响应头数据,再获得响应数据,最后拼成 Respoonse 对象返回。注意,当 responseType 是 "json" 时,响应数据返回的是 request.responseText,是个字符串,这会在下一步处理。
  3. 这里我们将拼接的 response 交由 settle 函数处理,并由它决定最终是成功请求(resolve(err))还是失败请求(reject(err)

settle 函数位于 lib/core/settle.js

/**
 * Resolve or reject a Promise based on response status.
 *
 * @param {Function} resolve A function that resolves the promise.
 * @param {Function} reject A function that rejects the promise.
 * @param {object} response The response.
 *
 * @returns {object} The response.
 */
export default function settle(resolve, reject, response) {
  // 1)
  const validateStatus = response.config.validateStatus;
  // 2)
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  // 3)
  } else {
    reject(new AxiosError(
      'Request failed with status code ' + response.status,
      [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4],
      response.config,
      response.request,
      response
    ));
  }
}
  1. 我们首先拿到了 validateStatus 配置。这是我们判断请求成功与否的关键
  2. 这个 if 通过就把传入的 response 直接丢出去,表示请求成功了。跟这个判断逻辑,我们可以知道,当 validateStatus 为空(nullundefined),所有响应都会认为是成功的被返回
  3. 否则,没有通过校验那就表示请求失败了。报错消息类似 'Request failed with status code xxx';4xx 状态码的返回 CODE 是 ERR_BAD_REQUEST,5xx 状态码的返回 CODE 是 ERR_BAD_RESPONSE;最后我们还把 response 作为 AxiosError 的 response 属性传入了进来

至此,我们就讲完了 axios 中的异常处理逻辑了。

总结

本文介绍了 axios 请求过程中可能会出现的各种异常场景。

axios 异常场景按照有无响应分 2 类:有响应异常和无响应异常。有响应异常就是指那些能成功接收到服务器响应状态码的请求,包括常见的 2xx、4xx 和 5xx;无响应异常则包括网络中断、无效地址、跨域错误、超时、取消等场景下的错误,这些都是接受不到服务器响应的。

然后,我们从浏览器端实现出发,介绍了 AxiosError、分析了抛出 AxiosError 异常的时机与方式。

希望本文的讲解,对大家理解 axios 处理错误会有更加深刻的理解,也能更加游刃有余的使用 axios。

感谢阅读,再见。