Express异步中间件被跳过,请求流入错误路由问题排查
Express.js中间件执行顺序异常及请求流入错误路由问题
问题描述
开发类Zotero应用时,遇到两个核心问题:
- 中间件未按设定顺序执行
- 请求意外流入错误路由处理逻辑
Collections路由代码
router .route('/') .post( controller.addLibraryToBody, controller.validateBody.create, controller.createOne, controller.sendResponse('create'), controller.debugLog );
相关中间件实现
父Controller类
moveReqKeyToBody(bodyKey: string, ...nestedReqKey: string[]) { return function (req: IRequest, res: Response, next: NextFunction) { let iterator: any = req; nestedReqKey.forEach(key => { if (iterator[key]) iterator = iterator[key]; else next(createError(400, `missing item from request: ${nestedReqKey}`)); }); req.body[bodyKey] = iterator; next(); }; } preventMaliciousBody(bodyValidationKeys: BodyValidationKeys) { let { mandatory = [], allowed = [] } = bodyValidationKeys; return function (req: Request, res: Response, next: NextFunction) { allowed = allowed.concat(mandatory); if ( mandatory.every(value => req.body[value]) && Object.keys(req.body).every(value => allowed.includes(value)) ) next(); else next(createError(400, 'invalid body')); }; } createOne = catchAsync( async ( req: IRemoveFieldsRequest, res: Response, next: NextFunction ): Promise<void> => { const document = await this.model.create(req.body); if (req.removeFields) { req.removeFields.forEach(field => { document[field] = undefined; }); } req[this.modelName] = document; next(); }; ); sendResponse = (operation: CRUD) => { return (req: IRequest, res: Response, next: NextFunction) => { switch (operation) { case 'create': res.status(201).json({ status: 'success', data: req[this.modelName] }); break; } }; }; debugLog(req: IRequest, res: Response, next: NextFunction) { console.log( `${Date.now()} - ${req.url} - ParamKeys: ${Object.keys( req.params )} - BodyKeys: ${Object.keys(req.body)}` ); next(); }
CollectionController子类
addLibraryToBody = this.moveReqKeyToBody('parent', 'library', 'id'); validateBody = { create: catchAsync( async (req: IRequest, res: Response, next: NextFunction) => { if (!req.body.type) req.body.type = collectionTypes.collection; this.preventMaliciousBody(this.bodyKeys.create)(req, res, next); if ( req.body.type === collectionTypes.searchingCollection && !req.body.searchQuery ) next(createError(400, 'invalid body')); else next(); } ) }
app.js配置
app .use('/api', apiRouter) .use( '*', function (req: Request, res: Response, next: NextFunction) { // todo fix this weird bug if (res.headersSent) { console.log(req.url); console.log(req.body); console.log(req.params); } else next(); }, Controller.unavailable ) .use(errorHandler);
Postman返回结果
{ "status": "success" }
服务器日志输出(含Morgan日志)
POST /api/libraries/6447a4c4dc088d6d43204668/collections 201 6.358 ms - 20 1683371139354 - / - ParamKeys: id - BodyKeys: name,parent,type / { name: 'norma coll', parent: '6447a4c4dc088d6d43204668', type: 'Collection' } { '0': '/api/libraries/6447a4c4dc088d6d43204668/collections' }
排查发现
在createOne方法中添加日志后,发现日志出现在Morgan日志前后,怀疑异步中间件未完全执行就进入后续逻辑。
问题分析与解决
1. validateBody.create的双重next()调用问题
validateBody.create中直接调用this.preventMaliciousBody(...)(req, res, next),该中间件本身会触发next(),后续代码又再次调用next(),导致两次调用next(),打乱中间件执行顺序,甚至让请求提前流入后续路由。
修复:用Promise包装验证逻辑,确保验证完成后再执行后续判断:
validateBody = { create: catchAsync( async (req: IRequest, res: Response, next: NextFunction) => { if (!req.body.type) req.body.type = collectionTypes.collection; // 包装为Promise,确保验证完成后再继续 await new Promise((resolve, reject) => { this.preventMaliciousBody(this.bodyKeys.create)(req, res, (err) => { if (err) reject(err); else resolve(); }); }); if ( req.body.type === collectionTypes.searchingCollection && !req.body.searchQuery ) { next(createError(400, 'invalid body')); } else { next(); } } ) }
2. sendResponse未终止请求
sendResponse发送响应后未终止请求,导致后续debugLog执行并调用next(),让请求流入*错误路由。
修复:发送响应后添加return终止请求:
sendResponse = (operation: CRUD) => { return (req: IRequest, res: Response, next: NextFunction) => { switch (operation) { case 'create': res.status(201).json({ status: 'success', data: req[this.modelName] }); return; // 终止请求,不再执行后续中间件 break; } }; };
3. *路由逻辑优化
原*路由在响应已发送时仍会执行后续逻辑,调整为仅在响应未发送时执行错误路由:
app .use('/api', apiRouter) .use(function (req: Request, res: Response, next: NextFunction) { if (!res.headersSent) { Controller.unavailable(req, res, next); } }) .use(errorHandler);
4. 确认catchAsync实现正确性
确保catchAsync能正确捕获异步逻辑中的错误并传递给next(),避免未捕获错误导致中间件顺序混乱。
内容的提问来源于stack exchange,提问作者Adjective
相关产品推荐
相关产品推荐

