Express服务中Promise无限循环异常行为原因排查
问题描述
现有如下Node.js服务脚本:
const got = require("got"); const getStream = require("get-stream"); const app = require("express")(); async function httpGet() { console.log("getting response"); let targetUrl = "http://localhost:3000/api"; let gotOptions = { method: "get", headers: { "content-type": "application/json" }, body: undefined, retries: 0 }; let response = await new Promise(async (resolve, reject) => { let stream = got.stream(targetUrl, gotOptions); stream.on("error", async error => { try { resolve(stream); } catch (err) { reject(err); } }); stream.on("response", async res => { try { resolve(await getStream(res)); } catch (err) { reject(err); } }) }); return response; } async function apiMiddleware(req, res, next) { try { const response = await httpGet(); console.log(response); } catch (e) { throw new Error("some error"); } next(); } app.use("/", [apiMiddleware]); app.get("/api", (req, res) => { res.json({ data: "some output" }); }); app.get("/someapp", (req, res) => { res.end(); }); app.listen(3000, () => { console.log("listening on port 3000"); });
访问localhost:3000/someapp路径时,控制台无限循环输出如下日志:
getting response getting response getting response ...
调试时发现Promise执行逻辑总是跳转回httpGet()函数起始位置,需要定位异常原因。
异常根因
本质是自调用递归导致的请求死循环,由两处逻辑共同触发:
- 中间件挂载范围过大
app.use("/", [apiMiddleware])把apiMiddleware注册成了根路径全局中间件,服务收到的所有请求,无论目标路径是/someapp还是/api,都会先经过这个中间件处理,才会进入对应路由的处理逻辑。 - 中间件逻辑触发自请求
该中间件执行时会调用httpGet(),向服务自身监听的http://localhost:3000/api发GET请求。这个新发出的请求到达本地3000端口的服务后,会再次触发全局中间件执行,再次调用httpGet()发起新的请求,整个递归过程没有终止条件,就形成了无限循环,因此控制台会反复打印getting response。调试时看到逻辑跳回httpGet()起始位置,就是新一轮递归请求触发的函数重入。
另外代码里还有两个会加重异常的隐患,虽然不是死循环的直接诱因:
- 给Promise构造函数传入
async执行器属于典型反模式:执行器内部异步逻辑抛出的错误无法被Promise自动捕获,很容易出现吞错误、Promise状态游离的问题。 - Express异步中间件的错误处理不符合规范:async函数内直接
throw的错误不会被Express默认错误处理逻辑捕获,会导致请求一直挂起,不会返回错误响应打断递归链路。
修复思路
- 收窄
apiMiddleware的挂载范围,不要作为全局中间件拦截所有请求,比如只挂载到需要校验的业务路由上,确保httpGet发出的/api请求不会再次触发该中间件,从根源切断递归链路。 - 修正Promise的写法,去掉多余的async执行器包装,同时给异步中间件加错误捕获逻辑,避免请求挂起。
内容的提问来源于stack exchange,提问作者milanHrabos
相关产品推荐
相关产品推荐

