You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 00:40:06