From c5f8770f0f8fd90459e35a829e18176c808cb0fc Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Fri, 16 Jan 2026 16:16:11 +0800 Subject: [PATCH 1/9] feat: support fetch clientOptions interceptors --- lib/core/fetch_factory.js | 19 ++- test/fixtures/apps/fetch-tracer/app.js | 72 +++++++++++ test/fixtures/apps/fetch-tracer/app/router.js | 17 +++ .../fetch-tracer/config/config.default.js | 6 + test/fixtures/apps/fetch-tracer/package.json | 3 + test/lib/core/fetch_tracer.test.js | 113 ++++++++++++++++++ 6 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 test/fixtures/apps/fetch-tracer/app.js create mode 100644 test/fixtures/apps/fetch-tracer/app/router.js create mode 100644 test/fixtures/apps/fetch-tracer/config/config.default.js create mode 100644 test/fixtures/apps/fetch-tracer/package.json create mode 100644 test/lib/core/fetch_tracer.test.js diff --git a/lib/core/fetch_factory.js b/lib/core/fetch_factory.js index 971bf941b6..9225118a57 100644 --- a/lib/core/fetch_factory.js +++ b/lib/core/fetch_factory.js @@ -14,14 +14,23 @@ if (mainNodejsVersion >= 20) { FetchFactory = urllib4.FetchFactory; debug('urllib4 enable'); - - fetch = function fetch(url, init) { + fetch = function(url, init) { if (!fetchInitialized) { const clientOptions = {}; if (this.config.httpclient?.lookup) { clientOptions.lookup = this.config.httpclient.lookup; } FetchFactory.setClientOptions(clientOptions); + + // Support custom interceptors via dispatcher.compose + // Must be set after setClientOptions because setClientOptions resets dispatcher + // interceptors is an array of interceptor functions that follow undici's dispatcher API(undici have not supported clientOptions.interceptors natively yet) + if (this.config.httpclient?.interceptors) { + const interceptors = this.config.httpclient.interceptors; + const originalDispatcher = FetchFactory.getDispatcher(); + FetchFactory.setDispatcher(originalDispatcher.compose(interceptors)); + } + fetchInitialized = true; } return FetchFactory.fetch(url, init); @@ -41,6 +50,12 @@ if (mainNodejsVersion >= 20) { } ssrfFetchFactory = new FetchFactory(); ssrfFetchFactory.setClientOptions(clientOptions); + + if (this.config.httpclient?.interceptors) { + const interceptors = this.config.httpclient.interceptors; + const originalDispatcher = ssrfFetchFactory.getDispatcher(); + ssrfFetchFactory.setDispatcher(originalDispatcher.compose(interceptors)); + } } return ssrfFetchFactory.fetch(url, init); }; diff --git a/test/fixtures/apps/fetch-tracer/app.js b/test/fixtures/apps/fetch-tracer/app.js new file mode 100644 index 0000000000..96c37e06f8 --- /dev/null +++ b/test/fixtures/apps/fetch-tracer/app.js @@ -0,0 +1,72 @@ +const assert = require('assert'); + +const TRACE_ID = Symbol('TRACE_ID'); +const RPC_ID = Symbol('RPC_ID'); + +// Simple Tracer implementation +class Tracer { + constructor(traceId, rpcId = '0') { + this.traceId = traceId; + this._rpcId = rpcId; + this._rpcIdSeq = 0; + } + + get rpcId() { + return this._rpcId; + } + + get rpcIdPlus() { + return `${this._rpcId}.${++this._rpcIdSeq}`; + } +} + +module.exports = class TracerApp { + constructor(app) { + this.app = app; + assert(app.config); + // Expose Tracer class for testing + app.Tracer = Tracer; + } + + configWillLoad() { + // Setup tracer interceptor using interceptors config + this.app.config.httpclient = this.app.config.httpclient || {}; + if (!this.app.FetchFactory) { + return; + } + const tracerConfig = this.app.config.tracer; + const HTTP_HEADER_TRACE_ID_KEY = tracerConfig.HTTP_HEADER_TRACE_ID_KEY.toLowerCase(); + const HTTP_HEADER_RPC_ID_KEY = tracerConfig.HTTP_HEADER_RPC_ID_KEY.toLowerCase(); + + this.app.config.httpclient.interceptors = [ + dispatch => { + const app = this.app; + return async function tracerInterceptor(opts, handler) { + const tracer = app.currentContext?.tracer; + let traceId; + let rpcId; + + try { + if (tracer) { + traceId = opts.headers[HTTP_HEADER_TRACE_ID_KEY] = tracer.traceId; + rpcId = opts.headers[HTTP_HEADER_RPC_ID_KEY] = tracer.rpcIdPlus; + } + } catch (e) { + e.message = '[egg-tracelog] set tracer header failed: ' + e.message; + app.logger.warn(e); + } + + try { + return await dispatch(opts, handler); + } finally { + const opaque = handler.opaque; + if (opaque) { + opaque[TRACE_ID] = traceId; + opaque[RPC_ID] = rpcId; + } + } + }; + }, + ]; + } +}; diff --git a/test/fixtures/apps/fetch-tracer/app/router.js b/test/fixtures/apps/fetch-tracer/app/router.js new file mode 100644 index 0000000000..7572256147 --- /dev/null +++ b/test/fixtures/apps/fetch-tracer/app/router.js @@ -0,0 +1,17 @@ +module.exports = app => { + app.get('/test', async ctx => { + // Mock a tracer on the context using the Tracer class + ctx.tracer = new app.Tracer('test-trace-id-123', '0'); + + // Store the current context so fetch can access it + app.currentContext = ctx; + + // Make a fetch request + const response = await app.fetch(ctx.query.url); + + ctx.body = { + status: response.status, + ok: response.ok, + }; + }); +}; diff --git a/test/fixtures/apps/fetch-tracer/config/config.default.js b/test/fixtures/apps/fetch-tracer/config/config.default.js new file mode 100644 index 0000000000..a4ced7edeb --- /dev/null +++ b/test/fixtures/apps/fetch-tracer/config/config.default.js @@ -0,0 +1,6 @@ +exports.keys = 'test key'; + +exports.tracer = { + HTTP_HEADER_TRACE_ID_KEY: 'x-trace-id', + HTTP_HEADER_RPC_ID_KEY: 'x-rpc-id', +}; \ No newline at end of file diff --git a/test/fixtures/apps/fetch-tracer/package.json b/test/fixtures/apps/fetch-tracer/package.json new file mode 100644 index 0000000000..b4f4aaca1d --- /dev/null +++ b/test/fixtures/apps/fetch-tracer/package.json @@ -0,0 +1,3 @@ +{ + "name": "fetch-tracer" +} diff --git a/test/lib/core/fetch_tracer.test.js b/test/lib/core/fetch_tracer.test.js new file mode 100644 index 0000000000..051851d167 --- /dev/null +++ b/test/lib/core/fetch_tracer.test.js @@ -0,0 +1,113 @@ +const assert = require('node:assert'); +const http = require('http'); +const utils = require('../../utils'); + +describe('test/lib/core/fetch_tracer.test.js', () => { + const version = utils.getNodeVersion(); + if (version < 20) return; + + let app; + let mockServer; + let receivedHeaders; + + before(async () => { + // Create a mock server to capture headers + mockServer = http.createServer((req, res) => { + receivedHeaders = req.headers; + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + }); + + await new Promise(resolve => { + mockServer.listen(0, '127.0.0.1', resolve); + }); + + app = utils.app('apps/fetch-tracer'); + await app.ready(); + }); + + after(() => { + if (mockServer?.listening) { + mockServer.close(); + } + }); + + afterEach(() => { + receivedHeaders = null; + }); + + it('should add tracer headers when fetch is called', async () => { + const port = mockServer.address().port; + const targetUrl = `http://127.0.0.1:${port}/mock`; + + const response = await app.httpRequest() + .get('/test') + .query({ url: targetUrl }) + .expect(200); + + assert.strictEqual(response.body.status, 200); + assert.strictEqual(response.body.ok, true); + + // Verify tracer headers were added with incremented rpcId + assert.strictEqual(receivedHeaders['x-trace-id'], 'test-trace-id-123'); + assert.strictEqual(receivedHeaders['x-rpc-id'], '0.1'); // rpcIdPlus increments from 0 + }); + + it('should work when tracer is not set', async () => { + // Clear currentContext + app.currentContext = null; + + const port = mockServer.address().port; + const targetUrl = `http://127.0.0.1:${port}/mock`; + + const response = await app.fetch(targetUrl); + + assert.strictEqual(response.status, 200); + + // Verify no tracer headers when tracer is not set + assert.strictEqual(receivedHeaders['x-trace-id'], undefined); + assert.strictEqual(receivedHeaders['x-rpc-id'], undefined); + }); + + + it('should handle fetch before configDidLoad completes', async () => { + // Test that lazy initialization preserves interceptors set in configDidLoad + const port = mockServer.address().port; + const targetUrl = `http://127.0.0.1:${port}/mock`; + + const ctx = app.mockContext(); + ctx.tracer = new app.Tracer('early-trace-id', '0.1'); + app.currentContext = ctx; + + const response = await app.fetch(targetUrl); + + assert.strictEqual(response.status, 200); + assert.strictEqual(receivedHeaders['x-trace-id'], 'early-trace-id'); + assert.strictEqual(receivedHeaders['x-rpc-id'], '0.1.1'); // rpcIdPlus increments from 0.1 + }); + + it('should increment rpcId on multiple fetch calls', async () => { + // Test that rpcId increments properly on each fetch + const port = mockServer.address().port; + const targetUrl = `http://127.0.0.1:${port}/mock`; + + const ctx = app.mockContext(); + ctx.tracer = new app.Tracer('multi-trace-id', '0'); + app.currentContext = ctx; + + // First fetch + await app.fetch(targetUrl); + assert.strictEqual(receivedHeaders['x-trace-id'], 'multi-trace-id'); + assert.strictEqual(receivedHeaders['x-rpc-id'], '0.1'); + + // Second fetch + await app.fetch(targetUrl); + assert.strictEqual(receivedHeaders['x-trace-id'], 'multi-trace-id'); + assert.strictEqual(receivedHeaders['x-rpc-id'], '0.2'); + + // Third fetch + await app.fetch(targetUrl); + assert.strictEqual(receivedHeaders['x-trace-id'], 'multi-trace-id'); + assert.strictEqual(receivedHeaders['x-rpc-id'], '0.3'); + }); +}); From 602ef505383a61ab66d477b0657e18d5449d8659 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Fri, 16 Jan 2026 16:54:04 +0800 Subject: [PATCH 2/9] chore: interceptor type --- index.d.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index 8471bc054a..fe1f84ea82 100644 --- a/index.d.ts +++ b/index.d.ts @@ -295,6 +295,10 @@ declare module 'egg' { maxFreeSockets?: number; } + type Dispatcher = FetchFactory['getDispatcher'] extends () => infer R + ? R + : never; + /** HttpClient config */ export interface HttpClientConfig extends HttpClientBaseConfig { /** http.Agent */ @@ -319,8 +323,8 @@ declare module 'egg' { allowH2?: boolean; /** Custom lookup function for DNS resolution */ lookup?: LookupFunction; + interceptors?: Dispatcher.DispatcherComposeInterceptor[]; } - export interface EggAppConfig { workerStartTimeout: number; baseDir: string; From ce24bbb61db618d984f9908b16d3ada9850be038 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Sat, 17 Jan 2026 16:42:33 +0800 Subject: [PATCH 3/9] chore: fix tsd --- index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.d.ts b/index.d.ts index fe1f84ea82..28ed159299 100644 --- a/index.d.ts +++ b/index.d.ts @@ -323,7 +323,7 @@ declare module 'egg' { allowH2?: boolean; /** Custom lookup function for DNS resolution */ lookup?: LookupFunction; - interceptors?: Dispatcher.DispatcherComposeInterceptor[]; + interceptors?: Parameters; } export interface EggAppConfig { workerStartTimeout: number; From 53267db71c5c4a33caf55e7f50d563d3065a3ffc Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Sat, 17 Jan 2026 17:07:06 +0800 Subject: [PATCH 4/9] fix: test case --- test/fixtures/apps/fetch-tracer/app/router.js | 5 ++ test/lib/core/fetch_tracer.test.js | 49 ++++++++++--------- 2 files changed, 31 insertions(+), 23 deletions(-) diff --git a/test/fixtures/apps/fetch-tracer/app/router.js b/test/fixtures/apps/fetch-tracer/app/router.js index 7572256147..fee502681d 100644 --- a/test/fixtures/apps/fetch-tracer/app/router.js +++ b/test/fixtures/apps/fetch-tracer/app/router.js @@ -9,6 +9,11 @@ module.exports = app => { // Make a fetch request const response = await app.fetch(ctx.query.url); + const traceId = response.headers.get('x-trace-id'); + if (traceId) ctx.set('x-trace-id', traceId); + const rpcId = response.headers.get('x-rpc-id'); + if (rpcId) ctx.set('x-rpc-id', rpcId); + ctx.body = { status: response.status, ok: response.ok, diff --git a/test/lib/core/fetch_tracer.test.js b/test/lib/core/fetch_tracer.test.js index 051851d167..45c3b2dc06 100644 --- a/test/lib/core/fetch_tracer.test.js +++ b/test/lib/core/fetch_tracer.test.js @@ -8,13 +8,21 @@ describe('test/lib/core/fetch_tracer.test.js', () => { let app; let mockServer; - let receivedHeaders; before(async () => { // Create a mock server to capture headers mockServer = http.createServer((req, res) => { - receivedHeaders = req.headers; - res.writeHead(200, { 'Content-Type': 'application/json' }); + const headers = { + 'Content-Type': 'application/json', + }; + if (req.headers['x-trace-id']) { + headers['x-trace-id'] = req.headers['x-trace-id']; + } + if (req.headers['x-rpc-id']) { + headers['x-rpc-id'] = req.headers['x-rpc-id']; + } + + res.writeHead(200, headers); res.end(JSON.stringify({ ok: true })); }); @@ -32,10 +40,6 @@ describe('test/lib/core/fetch_tracer.test.js', () => { } }); - afterEach(() => { - receivedHeaders = null; - }); - it('should add tracer headers when fetch is called', async () => { const port = mockServer.address().port; const targetUrl = `http://127.0.0.1:${port}/mock`; @@ -49,8 +53,8 @@ describe('test/lib/core/fetch_tracer.test.js', () => { assert.strictEqual(response.body.ok, true); // Verify tracer headers were added with incremented rpcId - assert.strictEqual(receivedHeaders['x-trace-id'], 'test-trace-id-123'); - assert.strictEqual(receivedHeaders['x-rpc-id'], '0.1'); // rpcIdPlus increments from 0 + assert.strictEqual(response.headers['x-trace-id'], 'test-trace-id-123'); + assert.strictEqual(response.headers['x-rpc-id'], '0.1'); // rpcIdPlus increments from 0 }); it('should work when tracer is not set', async () => { @@ -65,8 +69,8 @@ describe('test/lib/core/fetch_tracer.test.js', () => { assert.strictEqual(response.status, 200); // Verify no tracer headers when tracer is not set - assert.strictEqual(receivedHeaders['x-trace-id'], undefined); - assert.strictEqual(receivedHeaders['x-rpc-id'], undefined); + assert.strictEqual(response.headers.get('x-trace-id'), null); + assert.strictEqual(response.headers.get('x-rpc-id'), null); }); @@ -80,10 +84,9 @@ describe('test/lib/core/fetch_tracer.test.js', () => { app.currentContext = ctx; const response = await app.fetch(targetUrl); - assert.strictEqual(response.status, 200); - assert.strictEqual(receivedHeaders['x-trace-id'], 'early-trace-id'); - assert.strictEqual(receivedHeaders['x-rpc-id'], '0.1.1'); // rpcIdPlus increments from 0.1 + assert.strictEqual(response.headers.get('x-trace-id'), 'early-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.1.1'); // rpcIdPlus increments from 0.1 }); it('should increment rpcId on multiple fetch calls', async () => { @@ -96,18 +99,18 @@ describe('test/lib/core/fetch_tracer.test.js', () => { app.currentContext = ctx; // First fetch - await app.fetch(targetUrl); - assert.strictEqual(receivedHeaders['x-trace-id'], 'multi-trace-id'); - assert.strictEqual(receivedHeaders['x-rpc-id'], '0.1'); + let response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.1'); // Second fetch - await app.fetch(targetUrl); - assert.strictEqual(receivedHeaders['x-trace-id'], 'multi-trace-id'); - assert.strictEqual(receivedHeaders['x-rpc-id'], '0.2'); + response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.2'); // Third fetch - await app.fetch(targetUrl); - assert.strictEqual(receivedHeaders['x-trace-id'], 'multi-trace-id'); - assert.strictEqual(receivedHeaders['x-rpc-id'], '0.3'); + response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.3'); }); }); From eddb192496931b0e5707b10d6fea833a95fec032 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Sat, 17 Jan 2026 19:55:19 +0800 Subject: [PATCH 5/9] fix: test case --- test/fixtures/apps/fetch-tracer/app.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test/fixtures/apps/fetch-tracer/app.js b/test/fixtures/apps/fetch-tracer/app.js index 96c37e06f8..b0f9c182e1 100644 --- a/test/fixtures/apps/fetch-tracer/app.js +++ b/test/fixtures/apps/fetch-tracer/app.js @@ -46,10 +46,32 @@ module.exports = class TracerApp { let traceId; let rpcId; + function setHeader(key, value) { + if (!opts.headers) { + opts.headers = {}; + } + const headers = opts.headers; + if (typeof headers.set === 'function') { + headers.set(key, value); + return; + } + if (Array.isArray(headers)) { + if (headers.length > 0 && Array.isArray(headers[0])) { + headers.push([ key, value ]); + } else { + headers.push(key, value); + } + return; + } + headers[key] = value; + } + try { if (tracer) { - traceId = opts.headers[HTTP_HEADER_TRACE_ID_KEY] = tracer.traceId; - rpcId = opts.headers[HTTP_HEADER_RPC_ID_KEY] = tracer.rpcIdPlus; + traceId = tracer.traceId; + setHeader(HTTP_HEADER_TRACE_ID_KEY, traceId); + rpcId = tracer.rpcIdPlus; + setHeader(HTTP_HEADER_RPC_ID_KEY, rpcId); } } catch (e) { e.message = '[egg-tracelog] set tracer header failed: ' + e.message; From 693cd3714214fd39042413c81f9ec603662286b0 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Sat, 17 Jan 2026 21:10:08 +0800 Subject: [PATCH 6/9] fix: test case fetch ctx --- test/fixtures/apps/fetch-tracer/app.js | 7 ++- test/fixtures/apps/fetch-tracer/app/router.js | 26 +++++------ test/lib/core/fetch_tracer.test.js | 43 ++++++++++--------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/test/fixtures/apps/fetch-tracer/app.js b/test/fixtures/apps/fetch-tracer/app.js index b0f9c182e1..27c5d289b9 100644 --- a/test/fixtures/apps/fetch-tracer/app.js +++ b/test/fixtures/apps/fetch-tracer/app.js @@ -1,4 +1,5 @@ const assert = require('assert'); +const { AsyncLocalStorage } = require('async_hooks'); const TRACE_ID = Symbol('TRACE_ID'); const RPC_ID = Symbol('RPC_ID'); @@ -26,6 +27,8 @@ module.exports = class TracerApp { assert(app.config); // Expose Tracer class for testing app.Tracer = Tracer; + // Use AsyncLocalStorage for proper context isolation + app.ctxStorage = new AsyncLocalStorage(); } configWillLoad() { @@ -42,7 +45,9 @@ module.exports = class TracerApp { dispatch => { const app = this.app; return async function tracerInterceptor(opts, handler) { - const tracer = app.currentContext?.tracer; + // Use AsyncLocalStorage to get context instead of global variable + const ctx = app.ctxStorage.getStore() || app.currentContext; + const tracer = ctx?.tracer; let traceId; let rpcId; diff --git a/test/fixtures/apps/fetch-tracer/app/router.js b/test/fixtures/apps/fetch-tracer/app/router.js index fee502681d..1df6792b0a 100644 --- a/test/fixtures/apps/fetch-tracer/app/router.js +++ b/test/fixtures/apps/fetch-tracer/app/router.js @@ -3,20 +3,20 @@ module.exports = app => { // Mock a tracer on the context using the Tracer class ctx.tracer = new app.Tracer('test-trace-id-123', '0'); - // Store the current context so fetch can access it - app.currentContext = ctx; + // Use AsyncLocalStorage to store context for proper isolation + await app.ctxStorage.run(ctx, async () => { + // Make a fetch request + const response = await app.fetch(ctx.query.url); - // Make a fetch request - const response = await app.fetch(ctx.query.url); + const traceId = response.headers.get('x-trace-id'); + if (traceId) ctx.set('x-trace-id', traceId); + const rpcId = response.headers.get('x-rpc-id'); + if (rpcId) ctx.set('x-rpc-id', rpcId); - const traceId = response.headers.get('x-trace-id'); - if (traceId) ctx.set('x-trace-id', traceId); - const rpcId = response.headers.get('x-rpc-id'); - if (rpcId) ctx.set('x-rpc-id', rpcId); - - ctx.body = { - status: response.status, - ok: response.ok, - }; + ctx.body = { + status: response.status, + ok: response.ok, + }; + }); }); }; diff --git a/test/lib/core/fetch_tracer.test.js b/test/lib/core/fetch_tracer.test.js index 45c3b2dc06..0b83bd7299 100644 --- a/test/lib/core/fetch_tracer.test.js +++ b/test/lib/core/fetch_tracer.test.js @@ -58,12 +58,11 @@ describe('test/lib/core/fetch_tracer.test.js', () => { }); it('should work when tracer is not set', async () => { - // Clear currentContext - app.currentContext = null; - + // Test without tracer context const port = mockServer.address().port; const targetUrl = `http://127.0.0.1:${port}/mock`; + // Call fetch without any context const response = await app.fetch(targetUrl); assert.strictEqual(response.status, 200); @@ -81,9 +80,10 @@ describe('test/lib/core/fetch_tracer.test.js', () => { const ctx = app.mockContext(); ctx.tracer = new app.Tracer('early-trace-id', '0.1'); - app.currentContext = ctx; - const response = await app.fetch(targetUrl); + const response = await app.ctxStorage.run(ctx, async () => { + return await app.fetch(targetUrl); + }); assert.strictEqual(response.status, 200); assert.strictEqual(response.headers.get('x-trace-id'), 'early-trace-id'); assert.strictEqual(response.headers.get('x-rpc-id'), '0.1.1'); // rpcIdPlus increments from 0.1 @@ -96,21 +96,22 @@ describe('test/lib/core/fetch_tracer.test.js', () => { const ctx = app.mockContext(); ctx.tracer = new app.Tracer('multi-trace-id', '0'); - app.currentContext = ctx; - - // First fetch - let response = await app.fetch(targetUrl); - assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); - assert.strictEqual(response.headers.get('x-rpc-id'), '0.1'); - - // Second fetch - response = await app.fetch(targetUrl); - assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); - assert.strictEqual(response.headers.get('x-rpc-id'), '0.2'); - - // Third fetch - response = await app.fetch(targetUrl); - assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); - assert.strictEqual(response.headers.get('x-rpc-id'), '0.3'); + + await app.ctxStorage.run(ctx, async () => { + // First fetch + let response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.1'); + + // Second fetch + response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.2'); + + // Third fetch + response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.3'); + }); }); }); From c2b1098a7d6878801f98dd3427c691f28a3f1624 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Sun, 18 Jan 2026 02:28:23 +0800 Subject: [PATCH 7/9] Revert "fix: test case" This reverts commit eddb192496931b0e5707b10d6fea833a95fec032. --- test/fixtures/apps/fetch-tracer/app.js | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/test/fixtures/apps/fetch-tracer/app.js b/test/fixtures/apps/fetch-tracer/app.js index 27c5d289b9..e44e45eac2 100644 --- a/test/fixtures/apps/fetch-tracer/app.js +++ b/test/fixtures/apps/fetch-tracer/app.js @@ -51,32 +51,10 @@ module.exports = class TracerApp { let traceId; let rpcId; - function setHeader(key, value) { - if (!opts.headers) { - opts.headers = {}; - } - const headers = opts.headers; - if (typeof headers.set === 'function') { - headers.set(key, value); - return; - } - if (Array.isArray(headers)) { - if (headers.length > 0 && Array.isArray(headers[0])) { - headers.push([ key, value ]); - } else { - headers.push(key, value); - } - return; - } - headers[key] = value; - } - try { if (tracer) { - traceId = tracer.traceId; - setHeader(HTTP_HEADER_TRACE_ID_KEY, traceId); - rpcId = tracer.rpcIdPlus; - setHeader(HTTP_HEADER_RPC_ID_KEY, rpcId); + traceId = opts.headers[HTTP_HEADER_TRACE_ID_KEY] = tracer.traceId; + rpcId = opts.headers[HTTP_HEADER_RPC_ID_KEY] = tracer.rpcIdPlus; } } catch (e) { e.message = '[egg-tracelog] set tracer header failed: ' + e.message; From d7bf2a2e71eb908fec6d34661a9010ee1039bfb9 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Sun, 18 Jan 2026 02:29:26 +0800 Subject: [PATCH 8/9] Revert "fix: test case fetch ctx" This reverts commit 693cd3714214fd39042413c81f9ec603662286b0. --- test/fixtures/apps/fetch-tracer/app.js | 7 +-- test/fixtures/apps/fetch-tracer/app/router.js | 26 +++++------ test/lib/core/fetch_tracer.test.js | 43 +++++++++---------- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/test/fixtures/apps/fetch-tracer/app.js b/test/fixtures/apps/fetch-tracer/app.js index e44e45eac2..96c37e06f8 100644 --- a/test/fixtures/apps/fetch-tracer/app.js +++ b/test/fixtures/apps/fetch-tracer/app.js @@ -1,5 +1,4 @@ const assert = require('assert'); -const { AsyncLocalStorage } = require('async_hooks'); const TRACE_ID = Symbol('TRACE_ID'); const RPC_ID = Symbol('RPC_ID'); @@ -27,8 +26,6 @@ module.exports = class TracerApp { assert(app.config); // Expose Tracer class for testing app.Tracer = Tracer; - // Use AsyncLocalStorage for proper context isolation - app.ctxStorage = new AsyncLocalStorage(); } configWillLoad() { @@ -45,9 +42,7 @@ module.exports = class TracerApp { dispatch => { const app = this.app; return async function tracerInterceptor(opts, handler) { - // Use AsyncLocalStorage to get context instead of global variable - const ctx = app.ctxStorage.getStore() || app.currentContext; - const tracer = ctx?.tracer; + const tracer = app.currentContext?.tracer; let traceId; let rpcId; diff --git a/test/fixtures/apps/fetch-tracer/app/router.js b/test/fixtures/apps/fetch-tracer/app/router.js index 1df6792b0a..fee502681d 100644 --- a/test/fixtures/apps/fetch-tracer/app/router.js +++ b/test/fixtures/apps/fetch-tracer/app/router.js @@ -3,20 +3,20 @@ module.exports = app => { // Mock a tracer on the context using the Tracer class ctx.tracer = new app.Tracer('test-trace-id-123', '0'); - // Use AsyncLocalStorage to store context for proper isolation - await app.ctxStorage.run(ctx, async () => { - // Make a fetch request - const response = await app.fetch(ctx.query.url); + // Store the current context so fetch can access it + app.currentContext = ctx; - const traceId = response.headers.get('x-trace-id'); - if (traceId) ctx.set('x-trace-id', traceId); - const rpcId = response.headers.get('x-rpc-id'); - if (rpcId) ctx.set('x-rpc-id', rpcId); + // Make a fetch request + const response = await app.fetch(ctx.query.url); - ctx.body = { - status: response.status, - ok: response.ok, - }; - }); + const traceId = response.headers.get('x-trace-id'); + if (traceId) ctx.set('x-trace-id', traceId); + const rpcId = response.headers.get('x-rpc-id'); + if (rpcId) ctx.set('x-rpc-id', rpcId); + + ctx.body = { + status: response.status, + ok: response.ok, + }; }); }; diff --git a/test/lib/core/fetch_tracer.test.js b/test/lib/core/fetch_tracer.test.js index 0b83bd7299..45c3b2dc06 100644 --- a/test/lib/core/fetch_tracer.test.js +++ b/test/lib/core/fetch_tracer.test.js @@ -58,11 +58,12 @@ describe('test/lib/core/fetch_tracer.test.js', () => { }); it('should work when tracer is not set', async () => { - // Test without tracer context + // Clear currentContext + app.currentContext = null; + const port = mockServer.address().port; const targetUrl = `http://127.0.0.1:${port}/mock`; - // Call fetch without any context const response = await app.fetch(targetUrl); assert.strictEqual(response.status, 200); @@ -80,10 +81,9 @@ describe('test/lib/core/fetch_tracer.test.js', () => { const ctx = app.mockContext(); ctx.tracer = new app.Tracer('early-trace-id', '0.1'); + app.currentContext = ctx; - const response = await app.ctxStorage.run(ctx, async () => { - return await app.fetch(targetUrl); - }); + const response = await app.fetch(targetUrl); assert.strictEqual(response.status, 200); assert.strictEqual(response.headers.get('x-trace-id'), 'early-trace-id'); assert.strictEqual(response.headers.get('x-rpc-id'), '0.1.1'); // rpcIdPlus increments from 0.1 @@ -96,22 +96,21 @@ describe('test/lib/core/fetch_tracer.test.js', () => { const ctx = app.mockContext(); ctx.tracer = new app.Tracer('multi-trace-id', '0'); - - await app.ctxStorage.run(ctx, async () => { - // First fetch - let response = await app.fetch(targetUrl); - assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); - assert.strictEqual(response.headers.get('x-rpc-id'), '0.1'); - - // Second fetch - response = await app.fetch(targetUrl); - assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); - assert.strictEqual(response.headers.get('x-rpc-id'), '0.2'); - - // Third fetch - response = await app.fetch(targetUrl); - assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); - assert.strictEqual(response.headers.get('x-rpc-id'), '0.3'); - }); + app.currentContext = ctx; + + // First fetch + let response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.1'); + + // Second fetch + response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.2'); + + // Third fetch + response = await app.fetch(targetUrl); + assert.strictEqual(response.headers.get('x-trace-id'), 'multi-trace-id'); + assert.strictEqual(response.headers.get('x-rpc-id'), '0.3'); }); }); From dad5dbeed7b8fc6ed2ca339c4532123d94410317 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Sun, 18 Jan 2026 02:30:26 +0800 Subject: [PATCH 9/9] fix: fetch initialization --- lib/core/fetch_factory.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/core/fetch_factory.js b/lib/core/fetch_factory.js index 9225118a57..cfc3bed858 100644 --- a/lib/core/fetch_factory.js +++ b/lib/core/fetch_factory.js @@ -3,7 +3,8 @@ const debug = require('util').debuglog('egg:lib:core:fetch_factory'); const mainNodejsVersion = parseInt(process.versions.node.split('.')[0]); let FetchFactory; let fetch; -let fetchInitialized = false; +// Track initialization per app instance by storing a WeakMap +const fetchInitializedMap = new WeakMap(); let safeFetch; let ssrfFetchFactory; @@ -15,7 +16,7 @@ if (mainNodejsVersion >= 20) { debug('urllib4 enable'); fetch = function(url, init) { - if (!fetchInitialized) { + if (!fetchInitializedMap.get(this)) { const clientOptions = {}; if (this.config.httpclient?.lookup) { clientOptions.lookup = this.config.httpclient.lookup; @@ -31,7 +32,7 @@ if (mainNodejsVersion >= 20) { FetchFactory.setDispatcher(originalDispatcher.compose(interceptors)); } - fetchInitialized = true; + fetchInitializedMap.set(this, true); } return FetchFactory.fetch(url, init); };