路由中间件报错Cannot set headers after they are sent to the client解决
解决Express校验中间件重复响应与控制器提前执行问题
最初的异步校验中间件实现
我最初编写了如下异步校验中间件:
export async function validationMiddleware<T extends object>( dtoClass: new () => T, validateTarget: string, req: Request, res: Response, next: NextFunction, ) { const dtoInstance = new dtoClass(); const request = { body: req.body, query: req.query, params: req.params, }; Object.assign(dtoInstance, request[validateTarget as keyof typeof request]); //request[validateTarget as keyof typeof request] = dtoInstance; const errors: ValidationError[] = await validate(dtoInstance, { forbidUnknownValues: true, whitelist: true, forbidNonWhitelisted: true, }); if (errors.length > 0) { const validationErrors = errors.map((error) => ({ [error.property]: Object.values(error.constraints || {}), })); const errorMessage = 'Invalid data provided'; const errorResponse = { message: errorMessage, errors: validationErrors }; return res.status(422).json(errorResponse); } next(); }
路由中这样调用:
userRoutes.post('/users/signup', (req, res, next) => { validationMiddleware(CreateUserDto, 'body', req, res, next); userController.signUp(req, res); });
出现的问题
- 当请求数据无校验错误时,会抛出
Cannot set headers after they are sent to the client错误 - 当请求数据有错误时,客户端能收到错误响应,但控制器方法
signUp仍会执行
我尝试了多种方法都没能解决:
- 处理
validate的Promise结果以避免异步函数,实现同步流程 - 引入
hasError变量,返回该值,仅在无错误时调用控制器方法 - 在无错误时通过else语句调用
next() - 发送响应后用return终止中间件
改用Promise.then处理异步校验的尝试
后来我修改了中间件,移除async/await,改用then处理validate的Promise:
export function validationMiddleware<T extends object>( dtoClass: new () => T, validateTarget: string, req: Request, res: Response, next: NextFunction, ) { console.log('validation'); let hasErrors = false; const dtoInstance = new dtoClass(); const request = { body: req.body, query: req.query, params: req.params, }; Object.assign(dtoInstance, request[validateTarget as keyof typeof request]); //request[validateTarget as keyof typeof request] = dtoInstance; //const errors: ValidationError[] = []; validate(dtoInstance, { forbidUnknownValues: true, whitelist: true, forbidNonWhitelisted: true, }).then((errors) => { if (errors.length > 0) { const validationErrors = errors.map((error) => ({ [error.property]: Object.values(error.constraints || {}), })); const errorMessage = 'Invalid data provided'; const errorResponse = { message: errorMessage, errors: validationErrors }; res.status(422).json(errorResponse); } else { next(); } }); }
但问题依旧:控制器方法还是会提前执行,因为校验的Promise解析完成时间晚于控制器方法的调用。
使用validateSync同步校验解决问题
最后发现class-validator的validate有同步版本validateSync,修改后的中间件核心逻辑如下:
const dtoInstance = new dtoClass(); const request = { body: req.body, query: req.query, params: req.params, }; Object.assign(dtoInstance, request[validateTarget as keyof typeof request]); //request[validateTarget as keyof typeof request] = dtoInstance; const errors: ValidationError[] = validateSync(dtoInstance, { forbidUnknownValues: true, whitelist: true, forbidNonWhitelisted: true, }); if (errors.length > 0) { console.log('validation'); const validationErrors = errors.map((error) => ({ [error.property]: Object.values(error.constraints || {}), })); const errorMessage = 'Invalid data provided'; const errorResponse = { message: errorMessage, errors: validationErrors }; res.status(422).json(errorResponse); } return errors.length > 0;
路由调用改为:
userRoutes.post('/users/signup', (req, res, next) => { if (!validationMiddleware(CreateUserDto, 'body', req, res, next)) userController.signUp(req, res); });
现在功能完全正常,实现了预期需求:校验请求,有错误则返回客户端,无错误则执行控制器逻辑,且校验逻辑未侵入控制器。
内容的提问来源于stack exchange,提问作者derstauner
相关产品推荐
相关产品推荐

