Express报错ERR_HTTP_HEADERS_SENT:重复响应问题求助
排查Express中ERR_HTTP_HEADERS_SENT错误(已添加return仍触发)
触发的错误信息:
Error [ERR_HTTP_HEADERS_SENT]: Cannot remove headers after they are sent to the client
问题背景
- 已为所有
res.json()/res.send()添加return语句,但使用Postman请求/register路由时仍触发上述错误 - 中间件基于对象定义,服务器监听前已转为原始函数
核心中间件代码
// first contact for all routes _app.all("*", (req, res, next) => { if(next == null || next == undefined) return res.json(null); // Has user logged in? info("Connection from: ", req.header("x-forwarded-for") || req.socket.remoteAddress); if (!hauth(req)) { // they havent res.locals.authed = false; if (req.route.path == NEW_AUTH_ROUTE) { // get user credentials, validate, and create new session for user. const auth = newauth(req); // returns new auth key if user credentials are valid, // otherwise an error prompting the user to login again. return res.json(auth == null ? ServerResponse.authFail : prepareResponsePayload(auth)); } // tells the console that the guest user is unauthorized, // and passes false to the next() functions authed parameter, indicating // the custom route defined that the user is un-authed. Some pages such as // the profile page or account page will reject access instantly if this field is false. warning("Un-authed user with auth header of: ", req.headers[AUTH_HEADER_NAME] || "NONE"); } else { res.statusCode = 200; // can be changed in next() function res.locals.authed = true; } /** * ----------------- * - READ HERE - * ----------------- * * All custom routes created in this file must return their json response. * The returned value can be anything, even null. It is good practice to be an object. * * The next function from your route also contains a field in the res object specifying * if the user is authenticated or not: * @see res.locals.authed * * If your return value is an error, examples like on line 25 show how to properly use them. * * @see ServerResponse houses static constants used for different responses. Includes errors. * @see ServerSideError An object wrapper for an error recieved from the server. * * @see prepareResponsePayload prepareResponsePayload will prepare whatever is returned from next() to be sent back. Please read its contents. * */ res.json(prepareResponsePayload(next())); }) // init routes for (const route of Object.values(routes)) { try { switch (route.method) { case "POST": { _app.post(route.method, route.func); break; } case "DELETE": { _app.delete(route.method, route.func); break; } case "ALL": { _app.all(route.method, route.func); break; } case "PUT": { _app.put(route.method, route.func); break; } default: { _app.get(route.method, route.func); break; } } info("Registered route '" + route.route + "'. "); } catch (e) { fatal("Route could not be established. Name: " + route.route) } } _app.listen(SERVER_LISTEN_PORT, () => { info("Server Listening on " + SERVER_LISTEN_PORT + "."); })
路由定义代码
/** * @type {Object.<string, Route>} */ module.exports.routes = {} /** * Route for pinging the server. Mostly for testing. */ module.exports.routes. ping = new Route("GET", "/ping", async (req, res, next) => null); // all we are looking for is a ping, so no payload body is provided. module.exports.routes. register = new Route("POST", "/register", async (req, res, next) => { try { info("Route..."); const { username, email, password } = req; if (!vparams(username, email, password)) return new ServerSideError(1, "Please provide all required fields."); if (!vname(username)) return new ServerSideError(2, "Invalid Username."); if (!vemail(email)) return new ServerSideError(3, "Invalid Email."); if (!vpass(password)) return new ServerSideError(4, "Invalid Password."); return new Promise((resolve, reject) => { sql_addRow(new SQLRow({ username: username, password: password, email: email }, SQLTables.WEB_USERS), (success, error) => { if (error || !success) resolve(new ServerSideError(ServerSideError.ErrorCodes.OTHER, "Something unexpected happened.")); resolve(newauth(req)); }); }); } catch (e) { return new ServerSideError(-2, "Unexpected Error."); } });
问题排查与修复方案
1. 路由注册参数错误(最直接原因)
在循环注册路由时,你错误地将route.method作为路径参数传递(比如_app.post(route.method, route.func)),正确的应该使用route.route作为路径。这会导致路由注册完全错误,实际请求时可能触发多次中间件/路由匹配,进而重复发送响应。
修复代码:
for (const route of Object.values(routes)) { try { switch (route.method) { case "POST": { _app.post(route.route, route.func); // 替换route.method为route.route break; } case "DELETE": { _app.delete(route.route, route.func); break; } case "ALL": { _app.all(route.route, route.func); break; } case "PUT": { _app.put(route.route, route.func); break; } default: { _app.get(route.route, route.func); break; } } info("Registered route '" + route.route + "'. "); } catch (e) { fatal("Route could not be established. Name: " + route.route) } }
2. 中间件异步逻辑处理错误
register路由是async函数,返回Promise,但全局中间件中直接同步调用next()并执行res.json,会导致Promise未resolve就发送响应,后续Promise完成后若有其他响应操作,会触发重复响应。
修复中间件的异步处理:
_app.all("*", async (req, res, next) => { if(next == null || next == undefined) return res.json(null); info("Connection from: ", req.header("x-forwarded-for") || req.socket.remoteAddress); if (!hauth(req)) { res.locals.authed = false; if (req.route.path == NEW_AUTH_ROUTE) { const auth = newauth(req); return res.json(auth == null ? ServerResponse.authFail : prepareResponsePayload(auth)); } warning("Un-authed user with auth header of: ", req.headers[AUTH_HEADER_NAME] || "NONE"); } else { res.statusCode = 200; res.locals.authed = true; } // 等待异步路由执行完成 const result = await next(); // 检查是否已发送响应,避免重复发送 if (!res.headersSent) { res.json(prepareResponsePayload(result)); } })
3. 响应发送链路冗余检查
确保整个请求链路中只有一次响应发送:全局中间件的res.json和路由函数的返回值处理不能重复触发响应。通过res.headersSent判断可以有效避免重复发送。
内容的提问来源于stack exchange,提问作者user18514542
相关产品推荐
相关产品推荐

