问题背景
在迭代一个基于 Node.js 和 Express 的Web应用时,遇到一个影响用户体验的问题:当第三方接口不可用或返回异常时,整个页面会直接跳转到500错误白屏页面,导致用户无法继续使用应用。
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、异常处理缺失:当第三方接口请求失败(网络问题、超时、接口返回5xx等)时,代码直接抛出错误,触发Express的错误处理中间件,导致页面跳转到500错误页
2、缺乏默认值处理:即使接口返回错误数据,也没有提供合理的默认值,导致前端无法正常显示
3、日志记录不完善:没有对请求的异常情况进行有效记录,不利于问题排查
解决方案
设计并实现了一个safeRequest工具函数,用于封装第三方接口请求,提供以下关键功能:
1、统一错误处理:对网络错误、服务端错误、客户端错误进行分类处理
2、默认值返回:当请求失败时返回默认值,避免页面白屏
3、详细日志记录:记录请求的详细信息,便于问题排查
4、超时控制:添加合理的请求超时设置,防止请求挂起
const request = require('request');
const logger = require('./logger');
/**
* 请求封装,对请求结果进行统一处理,并返回接口数据或默认值
* @param {object} options 请求参数
* @param {object} defaultValue 默认值
* @param {function} callback 回调函数
*/
module.exports = function safeRequest(options, defaultValue, callback) {
// 支持只传两个参数:options, callback(第三个参数可选)
if (typeof defaultValue === 'function') {
callback = defaultValue;
defaultValue = null;
}
const startTime = Date.now();
// 默认值
const finalDefaultValue = defaultValue || null;
// 添加默认超时 (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] 服务端错误: ${url},状态码: ${response.statusCode},耗时: ${duration}ms`);
return callback(null, finalDefaultValue);
}
if (response.statusCode >= 400) {
logger.info(`[safeRequest] 客户端错误: ${url},状态码: ${response.statusCode}`);
// 4xx 也返回默认值,不中断流程
return callback(null, finalDefaultValue);
}
// 尝试解析 JSON
try {
const data = JSON.parse(body);
// !!!注意:此处需根据实际接口返回状态进行修改
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},状态码: ${response.code},耗时: ${duration}ms`);
return callback(null, finalDefaultValue);
}
} catch (e) {
logger.warn(`[safeRequest] 响应解析失败: ${url},响应内容:`, body);
callback(null, finalDefaultValue);
}
});
};
使用示例
const safeRequest = require('../utils/safeRequest');
homeRouter.post("/", function (req, res) {
safeRequest({ url: '/demo' }, { data: [] }, function (error, resData) {
// ***逻辑处理
});
});
问题引申思考:
一、为什么 Express 错误处理中间件无法像 Axios 响应拦截器一样工作?
// Axios响应拦截器
axios.interceptors.response.use(
response => {
// 处理成功响应
return response;
},
error => {
// 处理错误响应
return Promise.reject(error);
}
);
Axios 的拦截器机制是基于 Promise 实现的,可以在请求完成(无论成功或失败)后,自动触发拦截器,实现对响应的统一处理。而在该项目中,当 request.get 发生错误时,错误对象 error 被传递到回调函数,但没有调用 next(error),因此 Express 的错误处理中间件不会被触发。但代码继续执行,比如:JSON.parse(body),由于 body 可能为 undefined 或无效数据,导致新的错误,最终触发 Express 的错误处理中间件,则被 app (即express()实例)捕获。
总结:
触发机制不同:Axios 的拦截器是"自动触发"的,而 Express 的错误处理中间件需要显式调用next(err)才能触发
错误传递方式不同:Axios 通过 Promise 链自动传递错误,Express 需要手动传递错误
二、为什么不能简单地在回调中调用 next(error)?
// 尝试在回调中调用 next(error):
request.get({ url: '/demo' }, function (error, response, body) {
if (error) {
return next(error); // 试图触发错误处理
}
res.json(JSON.parse(body));
});
但这样会引发另一个问题:当错误发生时,已经调用了 next(error),Express 会将请求交给错误处理中间件,而错误处理中间件会调用 res.status(500).render('500.html'),导致页面跳转到500页。这恰恰是此次需求变更要求避免的,因此需要采取调用封装函数进行异常捕获处理。