详解axios是如何处理异常的
作者:zhangbao90s
axios 中的正常请求
axios 中当请求服务正常返回时,会落入 .then() 方法中。
axios.get('https://httpstat.us/200') .then(res => { console.log(res) })
效果如下:
axios 会把响应结果包装在返回的 Response 对象的 data 属性中,除此之外:
- config:即请求配置
- headers:响应头数据(AxiosHeaders 对象)
- request:请求实例。浏览器环境就是 XMLHttpRequest 对象
- status:HTTP 状态码。本案例是 200,表示请求成功处理了
- 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); }
简单做一些说明:
- Error.call(this) 的作用类似调用父级构造函数,AxiosError 实例原型也成 Error 实例了
- 收集报错栈信息,优先以 Error.captureStackTrace 方式收集,方便排查问题
- 设置常规属性 message 和 name
- 扩展出 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
不管当前请求是否成功,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; }
- 这里做了 request 的非空判断。因为 onloadend 在调用之前,可以已经在其他的回调事件中处理了,直接返回即可
- 这里则是准备返回的响应数据。先收集响应头数据,再获得响应数据,最后拼成 Respoonse 对象返回。注意,当 responseType 是 "json" 时,响应数据返回的是 request.responseText,是个字符串,这会在下一步处理。
- 这里我们将拼接的 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 )); } }
- 我们首先拿到了 validateStatus 配置。这是我们判断请求成功与否的关键
- 这个 if 通过就把传入的 response 直接丢出去,表示请求成功了。跟这个判断逻辑,我们可以知道,当 validateStatus 为空(
null
或undefined
),所有响应都会认为是成功的被返回 - 否则,没有通过校验那就表示请求失败了。报错消息类似
'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处理异常的资料请关注脚本之家其它相关文章!