問題背景
在迭代一個基于 Node.js 和 Express 的Web應(yīng)用時,遇到一個影響用戶體驗的問題:當(dāng)?shù)谌浇涌诓豢捎没蚍祷禺惓r,整個頁面會直接跳轉(zhuǎn)到500錯誤白屏頁面,導(dǎo)致用戶無法繼續(xù)使用應(yīng)用。
var express = require('express')
var app = express()
app.use(function (err, req, res, next) {
logger.error(`[500] method: ${req.method}, url: ${req.url}`);
res.status(500).render('500.html',{
title:'500'
});
});
請求代碼如下:
// controller
var request = require("request");
var express = require("express")
var homeRouter = express.Router();
homeRouter.post("/", function (req, res) {
request.get({ url: '/demo' }, function (error, response, body) {
// ***邏輯處理
})
})
存在問題
1、異常處理缺失:當(dāng)?shù)谌浇涌谡埱笫。ňW(wǎng)絡(luò)問題、超時、接口返回5xx等)時,代碼直接拋出錯誤,觸發(fā)Express的錯誤處理中間件,導(dǎo)致頁面跳轉(zhuǎn)到500錯誤頁
2、缺乏默認(rèn)值處理:即使接口返回錯誤數(shù)據(jù),也沒有提供合理的默認(rèn)值,導(dǎo)致前端無法正常顯示
3、日志記錄不完善:沒有對請求的異常情況進(jìn)行有效記錄,不利于問題排查
解決方案
設(shè)計并實現(xiàn)了一個safeRequest工具函數(shù),用于封裝第三方接口請求,提供以下關(guān)鍵功能:
1、統(tǒng)一錯誤處理:對網(wǎng)絡(luò)錯誤、服務(wù)端錯誤、客戶端錯誤進(jìn)行分類處理
2、默認(rèn)值返回:當(dāng)請求失敗時返回默認(rèn)值,避免頁面白屏
3、詳細(xì)日志記錄:記錄請求的詳細(xì)信息,便于問題排查
4、超時控制:添加合理的請求超時設(shè)置,防止請求掛起
const request = require('request');
const logger = require('./logger');
/**
* 請求封裝,對請求結(jié)果進(jìn)行統(tǒng)一處理,并返回接口數(shù)據(jù)或默認(rèn)值
* @param {object} options 請求參數(shù)
* @param {object} defaultValue 默認(rèn)值
* @param {function} callback 回調(diào)函數(shù)
*/
module.exports = function safeRequest(options, defaultValue, callback) {
// 支持只傳兩個參數(shù):options, callback(第三個參數(shù)可選)
if (typeof defaultValue === 'function') {
callback = defaultValue;
defaultValue = null;
}
const startTime = Date.now();
// 默認(rèn)值
const finalDefaultValue = defaultValue || null;
// 添加默認(rèn)超時 (60秒)
const reqOptions = Object.assign({ timeout: 60 * 1000 }, options);
request(reqOptions, (error, response, body) => {
const duration = Date.now() - startTime;
const url = options.url || 'unknown';
if (error) {
logger.warn(`[safeRequest] 請求錯誤: ${url},耗時: ${duration}ms, 錯誤:`, error);
return callback(null, finalDefaultValue);
}
if (response.statusCode >= 500) {
logger.warn(`[safeRequest] 服務(wù)端錯誤: ${url},狀態(tài)碼: ${response.statusCode},耗時: ${duration}ms`);
return callback(null, finalDefaultValue);
}
if (response.statusCode >= 400) {
logger.info(`[safeRequest] 客戶端錯誤: ${url},狀態(tài)碼: ${response.statusCode}`);
// 4xx 也返回默認(rèn)值,不中斷流程
return callback(null, finalDefaultValue);
}
// 嘗試解析 JSON
try {
const data = JSON.parse(body);
// ?。?!注意:此處需根據(jù)實際接口返回狀態(tài)進(jìn)行修改
if (data.code === 200 || data.code === 8200 || data.success === true) {
logger.info(`[safeRequest] 請求成功: ${url},耗時: ${duration}ms`);
return callback(null, data);
} else {
logger.warn(`[safeRequest] 請求失敗: ${url},狀態(tài)碼: ${response.code},耗時: ${duration}ms`);
return callback(null, finalDefaultValue);
}
} catch (e) {
logger.warn(`[safeRequest] 響應(yīng)解析失敗: ${url},響應(yīng)內(nèi)容:`, body);
callback(null, finalDefaultValue);
}
});
};
使用示例
const safeRequest = require('../utils/safeRequest');
homeRouter.post("/", function (req, res) {
safeRequest({ url: '/demo' }, { data: [] }, function (error, resData) {
// ***邏輯處理
});
});
問題引申思考:
一、為什么 Express 錯誤處理中間件無法像 Axios 響應(yīng)攔截器一樣工作?
// Axios響應(yīng)攔截器
axios.interceptors.response.use(
response => {
// 處理成功響應(yīng)
return response;
},
error => {
// 處理錯誤響應(yīng)
return Promise.reject(error);
}
);
Axios 的攔截器機(jī)制是基于 Promise 實現(xiàn)的,可以在請求完成(無論成功或失敗)后,自動觸發(fā)攔截器,實現(xiàn)對響應(yīng)的統(tǒng)一處理。而在該項目中,當(dāng) request.get 發(fā)生錯誤時,錯誤對象 error 被傳遞到回調(diào)函數(shù),但沒有調(diào)用 next(error),因此 Express 的錯誤處理中間件不會被觸發(fā)。但代碼繼續(xù)執(zhí)行,比如:JSON.parse(body),由于 body 可能為 undefined 或無效數(shù)據(jù),導(dǎo)致新的錯誤,最終觸發(fā) Express 的錯誤處理中間件,則被 app (即express()實例)捕獲。
總結(jié):
觸發(fā)機(jī)制不同:Axios 的攔截器是"自動觸發(fā)"的,而 Express 的錯誤處理中間件需要顯式調(diào)用next(err)才能觸發(fā)
錯誤傳遞方式不同:Axios 通過 Promise 鏈自動傳遞錯誤,Express 需要手動傳遞錯誤
二、為什么不能簡單地在回調(diào)中調(diào)用 next(error)?
// 嘗試在回調(diào)中調(diào)用 next(error):
request.get({ url: '/demo' }, function (error, response, body) {
if (error) {
return next(error); // 試圖觸發(fā)錯誤處理
}
res.json(JSON.parse(body));
});
但這樣會引發(fā)另一個問題:當(dāng)錯誤發(fā)生時,已經(jīng)調(diào)用了 next(error),Express 會將請求交給錯誤處理中間件,而錯誤處理中間件會調(diào)用 res.status(500).render('500.html'),導(dǎo)致頁面跳轉(zhuǎn)到500頁。這恰恰是此次需求變更要求避免的,因此需要采取調(diào)用封裝函數(shù)進(jìn)行異常捕獲處理。