dd-trace埋点导致含for循环的Axios post请求失败时代码运行异常
Express接入dd-trace埋点后请求报错问题修复
问题背景
为了定位ExpressJS应用接入dd-trace埋点后的异常问题,迁移代码后出现两种相似逻辑的代码块表现不一致的情况:this.client为封装的Axios实例。
正常运行代码
无论POST请求成功或失败均可正常执行:
return (async (fn) => { try { return Promise.resolve(await fn()) } catch (e) { return Promise.reject(e) } return Promise.reject(new Error('Unusual Error')) })(() => this.client.post(`foo/bars`, data, opts))
异常代码
仅请求成功时正常,请求失败时dd埋点抛出异常且程序hang住(单次迭代for循环仅用于复现问题):
return (async (fn) => { for (let i = 0; i < 1; i++) { try { return Promise.resolve(await fn()) } catch (e) { return Promise.reject(e) } } return Promise.reject(new Error('Unusual Error')) })(() => this.client.post(`foo/bars`, data, opts))
报错栈信息
TypeError: Cannot read property 'push' of undefined at DatadogSpan._createContext (/app/node_modules/dd-trace/packages/dd-trace/src/opentracing/span.js:76:32) at new DatadogSpan (/app/node_modules/dd-trace/packages/dd-trace/src/opentracing/span.js:30:30) at DatadogTracer._startSpanInternal (/app/node_modules/dd-trace/packages/dd-trace/src/opentracing/tracer.js:73:18) at DatadogTracer._startSpan (/app/node_modules/dd-trace/packages/dd-trace/src/opentracing/tracer.js:62:17) at DatadogTracer.Tracer.startSpan (/app/node_modules/opentracing/lib/tracer.js:61:21) at Object.wrapMiddleware (/app/node_modules/dd-trace/packages/dd-trace/src/plugins/util/web.js:104:25) at callHandle (/app/node_modules/dd-trace/packages/datadog-plugin-router/src/index.js:111:14) at wrapCallHandle (/app/node_modules/dd-trace/packages/datadog-plugin-router/src/index.js:41:14) at Layer.handle_error (/app/node_modules/express/lib/router/layer.js:71:5) at trim_prefix (/app/node_modules/express/lib/router/index.js:315:13)
根因分析
报错本质是dd-trace的异步执行上下文丢失:
- dd-trace依赖Node.js的
async_hooks能力跟踪请求链路的span上下文,代码中手动给async函数返回值套Promise.resolve/Promise.reject的操作,会打断dd-trace对原始Axios请求Promise的链路埋点逻辑 - 叠加for循环的块级作用域影响,请求错误触发catch分支返回手动包装的reject Promise时,dd-trace尝试创建新span关联上下文,发现当前执行上下文的span存储对象未初始化,直接触发属性读取报错,未处理的上下文异常进一步阻塞事件循环导致程序hang住。
修复方案
方案1:移除不必要的Promise手动包装(优先推荐)
async函数本身的返回值会自动封装为Promise,直接返回结果/抛出错误即可,不需要手动调用Promise相关方法,修改后代码如下:
return (async (fn) => { for (let i = 0; i < 1; i++) { try { return await fn() } catch (e) { throw e } } throw new Error('Unusual Error') })(() => this.client.post(`foo/bars`, data, opts))
修改后dd-trace可以正常跟踪原始Axios请求的Promise链路,上下文不会丢失。
方案2:升级dd-trace版本
该上下文丢失属于dd-trace旧版本的已知问题,在3.0+稳定版已完成修复,直接升级依赖版本即可解决大部分场景的同类报错。
方案3:临时兼容配置
如果暂时无法升级版本或者修改业务代码,可以在dd-trace初始化时添加如下配置开启兼容模式:
const tracer = require('dd-trace').init({ // 保留原有其他配置 legacyAsyncHooks: true, trackAsyncResources: true })
该配置会启用旧版异步上下文跟踪逻辑,适配有特殊循环/闭包结构的代码场景。
内容的提问来源于stack exchange,提问作者Dave Newton
相关产品推荐
相关产品推荐

