From 4cde0f1a1a4a7320ce6cf139220b53db0a3aad8e Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Thu, 8 Jan 2026 18:19:26 +0800 Subject: [PATCH 1/9] feat: httpclient_next proxy --- lib/egg.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/egg.js b/lib/egg.js index 8bb0be2e7c..0c0c35ab77 100644 --- a/lib/egg.js +++ b/lib/egg.js @@ -297,13 +297,26 @@ class EggApplication extends EggCore { */ createHttpClient(options = {}) { let httpClient; - options.lookup = options.lookup ?? this.config.httpclient.lookup; - + let realClient = null; if (this.config.httpclient.useHttpClientNext || this.config.httpclient.allowH2) { - httpClient = new this.HttpClientNext(this, options); + const self = this; + httpClient = new Proxy({}, { + get(_target, prop) { + if (!realClient) { + options.lookup = options.lookup ?? self.config.httpclient.lookup; + realClient = new self.HttpClientNext(self, options); + } + const value = realClient[prop]; + if (typeof value === 'function') { + return value.bind(realClient); + } + return value; + }, + }); } else if (this.config.httpclient?.enableDNSCache) { httpClient = new DNSCacheHttpClient(this, options); } else { + options.lookup = options.lookup ?? this.config.httpclient.lookup; httpClient = new this.HttpClient(this, options); } return httpClient; From 581cac853b0ff0e42db60064486381e934cf52e3 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Thu, 8 Jan 2026 18:36:57 +0800 Subject: [PATCH 2/9] fix: httpclient proxy --- lib/egg.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/egg.js b/lib/egg.js index 0c0c35ab77..ca88282339 100644 --- a/lib/egg.js +++ b/lib/egg.js @@ -312,6 +312,14 @@ class EggApplication extends EggCore { } return value; }, + set(_target, prop, value) { + if (!realClient) { + options.lookup = options.lookup ?? self.config.httpclient.lookup; + realClient = new self.HttpClientNext(self, options); + } + realClient[prop] = value; + return true; + } }); } else if (this.config.httpclient?.enableDNSCache) { httpClient = new DNSCacheHttpClient(this, options); From a59d6bd1a505c51f001355cea49f344ec3dc4799 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Thu, 8 Jan 2026 18:41:02 +0800 Subject: [PATCH 3/9] style: lint fix --- lib/egg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/egg.js b/lib/egg.js index ca88282339..74b51b414d 100644 --- a/lib/egg.js +++ b/lib/egg.js @@ -319,7 +319,7 @@ class EggApplication extends EggCore { } realClient[prop] = value; return true; - } + }, }); } else if (this.config.httpclient?.enableDNSCache) { httpClient = new DNSCacheHttpClient(this, options); From 7161a4a1060cb6213039bf7bb9d1bd8e01872357 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Fri, 9 Jan 2026 18:14:34 +0800 Subject: [PATCH 4/9] feat: add dns error test case --- test/lib/core/dns_resolver.test.js | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/lib/core/dns_resolver.test.js b/test/lib/core/dns_resolver.test.js index 1e58a8173f..ea46e3c3da 100644 --- a/test/lib/core/dns_resolver.test.js +++ b/test/lib/core/dns_resolver.test.js @@ -1,4 +1,7 @@ const utils = require('../../utils'); +const mm = require('egg-mock'); +const dns = require('dns'); +const dnsPromise = require('dns').promises; const assert = require('assert'); describe('test/lib/core/dns_resolver.test.js', () => { @@ -22,6 +25,8 @@ describe('test/lib/core/dns_resolver.test.js', () => { url2 = url2.replace('127.0.0.2', 'localhost'); }); + afterEach(mm.restore); + after(() => { if (server1?.server?.listening) server1.server.close(); if (server2?.server?.listening) server2.server.close(); @@ -76,4 +81,69 @@ describe('test/lib/core/dns_resolver.test.js', () => { assert(err.message.includes('fetch failed')); } }); + + it('should not fail even if dns.lookup fail, because lookup is overridden', async () => { + mm.error(dnsPromise, 'lookup', 'mock dns lookup error'); + const res = await app.curl(url2 + '/get_headers', { dataType: 'json' }); + assert(res.status === 200); + }); }); + +describe('test/lib/core/dns_resolver with dns error', () => { + let server; + let cache = new Map() + before(async () => { + server = await utils.startNewLocalServer('127.0.0.1'); + if (!server) { + throw new Error('start local server failed'); + } + app = utils.app('apps/dns_resolver'); + await app.ready(); + app.config.httpclient.lookup = function (hostname, options, callback) { + if (cache.has(hostname)) { + const address = cache.get(hostname); + callback(null, address, 4); + return; + } else { + dns.lookup(hostname, options, (err, address, family) => { + if (!err) { + cache.set(hostname, address); + callback(null, address, family); + } else { + callback(err); + } + }); + } + } + url = server.url; + url = url.replace('127.0.0.1', 'localhost'); + }); + + afterEach(mm.restore); + + after(() => { + if (server?.server?.listening) server.server.close(); + }); + + it('should curl work', async () => { + const res = await app.curl(url + '/get_headers', { dataType: 'json' }); + assert(res.status === 200); + assert(cache.has('localhost')); + }) + + it('should cache work when dns fails', async () => { + mm.error(dns, 'lookup', 'mock dns lookup error'); + const res = await app.curl(url + '/get_headers', { dataType: 'json' }); + assert(res.status === 200); + assert(cache.has('localhost')); + cache.delete('localhost'); + // should fail now + try { + await app.curl(url + '/get_headers', { dataType: 'json' }); + } catch (err) { + assert(err); + assert(err.message.includes('mock dns lookup error')); + } + }) +}) + From 6a212b81bfbfbad0fa3e34a6d03af016f0e9182a Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Fri, 9 Jan 2026 18:15:54 +0800 Subject: [PATCH 5/9] styles: lint fix --- test/lib/core/dns_resolver.test.js | 33 ++++++++++++++++-------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/test/lib/core/dns_resolver.test.js b/test/lib/core/dns_resolver.test.js index ea46e3c3da..c9ab721b9b 100644 --- a/test/lib/core/dns_resolver.test.js +++ b/test/lib/core/dns_resolver.test.js @@ -91,7 +91,10 @@ describe('test/lib/core/dns_resolver.test.js', () => { describe('test/lib/core/dns_resolver with dns error', () => { let server; - let cache = new Map() + let app; + let url; + + const cache = new Map(); before(async () => { server = await utils.startNewLocalServer('127.0.0.1'); if (!server) { @@ -99,22 +102,22 @@ describe('test/lib/core/dns_resolver with dns error', () => { } app = utils.app('apps/dns_resolver'); await app.ready(); - app.config.httpclient.lookup = function (hostname, options, callback) { + app.config.httpclient.lookup = function(hostname, options, callback) { if (cache.has(hostname)) { const address = cache.get(hostname); callback(null, address, 4); return; - } else { - dns.lookup(hostname, options, (err, address, family) => { - if (!err) { - cache.set(hostname, address); - callback(null, address, family); - } else { - callback(err); - } - }); } - } + dns.lookup(hostname, options, (err, address, family) => { + if (!err) { + cache.set(hostname, address); + callback(null, address, family); + } else { + callback(err); + } + }); + + }; url = server.url; url = url.replace('127.0.0.1', 'localhost'); }); @@ -129,7 +132,7 @@ describe('test/lib/core/dns_resolver with dns error', () => { const res = await app.curl(url + '/get_headers', { dataType: 'json' }); assert(res.status === 200); assert(cache.has('localhost')); - }) + }); it('should cache work when dns fails', async () => { mm.error(dns, 'lookup', 'mock dns lookup error'); @@ -144,6 +147,6 @@ describe('test/lib/core/dns_resolver with dns error', () => { assert(err); assert(err.message.includes('mock dns lookup error')); } - }) -}) + }); +}); From c38f7bf123067f373e76357a39ef457b3e1c7c69 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Fri, 9 Jan 2026 18:55:46 +0800 Subject: [PATCH 6/9] fix: test case for dns cache --- test/lib/core/dns_resolver.test.js | 74 +++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git a/test/lib/core/dns_resolver.test.js b/test/lib/core/dns_resolver.test.js index c9ab721b9b..84783626bd 100644 --- a/test/lib/core/dns_resolver.test.js +++ b/test/lib/core/dns_resolver.test.js @@ -93,6 +93,7 @@ describe('test/lib/core/dns_resolver with dns error', () => { let server; let app; let url; + let originalDNSServers; const cache = new Map(); before(async () => { @@ -104,49 +105,90 @@ describe('test/lib/core/dns_resolver with dns error', () => { await app.ready(); app.config.httpclient.lookup = function(hostname, options, callback) { if (cache.has(hostname)) { - const address = cache.get(hostname); - callback(null, address, 4); + const record = cache.get(hostname); + if (options && options.all) { + const addresses = record.map(r => ({ address: r.address, family: 4 })); + callback(null, addresses); + return; + } + callback(null, [ record[0].address ], 4); return; } - dns.lookup(hostname, options, (err, address, family) => { - if (!err) { - cache.set(hostname, address); - callback(null, address, family); + dnsPromise.resolve4(hostname, { ttl: true }).then(addresses => { + if (addresses && addresses.length !== 0) { + if (Array.isArray(addresses)) { + cache.set(hostname, addresses); + } else { + cache.set(hostname, [ addresses ]); + } + if (options && options.all) { + const addrList = addresses.map(r => ({ address: r.address, family: 4 })); + callback(null, addrList); + return; + } + callback(null, [ addresses[0].address ], 4); } else { - callback(err); + callback(new Error('no addresses found')); } + }).catch(err => { + callback(err); }); }; url = server.url; url = url.replace('127.0.0.1', 'localhost'); + originalDNSServers = dns.promises.getServers(); + dns.promises.setServers([ '223.5.5.5', '223.6.6.6' ]); }); afterEach(mm.restore); after(() => { + dns.promises.setServers(originalDNSServers); if (server?.server?.listening) server.server.close(); }); - it('should curl work', async () => { - const res = await app.curl(url + '/get_headers', { dataType: 'json' }); - assert(res.status === 200); - assert(cache.has('localhost')); - }); it('should cache work when dns fails', async () => { - mm.error(dns, 'lookup', 'mock dns lookup error'); - const res = await app.curl(url + '/get_headers', { dataType: 'json' }); - assert(res.status === 200); + const res1 = await app.curl(url + '/get_headers', { dataType: 'json' }); + assert(res1.status === 200); assert(cache.has('localhost')); + + mm.error(dnsPromise, 'resolve4', 'mock dns lookup error'); + const res2 = await app.curl(url + '/get_headers', { dataType: 'json' }); + assert(res2.status === 200); cache.delete('localhost'); // should fail now + // assert.error(app.curl(url + '/get_headers', { dataType: 'json' })) + let shouldFail = false; try { await app.curl(url + '/get_headers', { dataType: 'json' }); } catch (err) { - assert(err); + shouldFail = true; assert(err.message.includes('mock dns lookup error')); } + assert(shouldFail); + }); + + it('should cache work when name server fails', async () => { + const successRes = await app.curl(url + '/get_headers', { dataType: 'json' }); + assert(successRes.status === 200); + assert(cache.has('localhost')); + // can't resolve localhost now, but cache still works + dns.promises.setServers([ '8.8.8.8' ]); + const res = await app.curl(url + '/get_headers', { dataType: 'json' }); + assert(res.status === 200); + + // clear cache, should fail now + cache.delete('localhost'); + let shouldFail = false; + try { + await app.curl(url + '/get_headers', { dataType: 'json' }); + } catch (err) { + shouldFail = true; + assert(err.message.includes('queryA ENOTFOUND localhost')); + } + assert(shouldFail); }); }); From 7cdae669ad8034c2d62fec50ad337b3fc66e56d1 Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Mon, 12 Jan 2026 11:00:48 +0800 Subject: [PATCH 7/9] fix: perfect httpclient_next proxy --- lib/egg.js | 70 +++++++++++++++++++++----------- test/lib/core/httpclient.test.js | 47 +++++++++++++++++++++ 2 files changed, 94 insertions(+), 23 deletions(-) diff --git a/lib/egg.js b/lib/egg.js index 74b51b414d..ba6c5162d6 100644 --- a/lib/egg.js +++ b/lib/egg.js @@ -297,30 +297,8 @@ class EggApplication extends EggCore { */ createHttpClient(options = {}) { let httpClient; - let realClient = null; if (this.config.httpclient.useHttpClientNext || this.config.httpclient.allowH2) { - const self = this; - httpClient = new Proxy({}, { - get(_target, prop) { - if (!realClient) { - options.lookup = options.lookup ?? self.config.httpclient.lookup; - realClient = new self.HttpClientNext(self, options); - } - const value = realClient[prop]; - if (typeof value === 'function') { - return value.bind(realClient); - } - return value; - }, - set(_target, prop, value) { - if (!realClient) { - options.lookup = options.lookup ?? self.config.httpclient.lookup; - realClient = new self.HttpClientNext(self, options); - } - realClient[prop] = value; - return true; - }, - }); + httpClient = this._createHttpClientNextProxy(options); } else if (this.config.httpclient?.enableDNSCache) { httpClient = new DNSCacheHttpClient(this, options); } else { @@ -330,6 +308,52 @@ class EggApplication extends EggCore { return httpClient; } + _createHttpClientNextProxy(options = {}) { + const self = this; + let realClient = null; + const init = () => { + if (realClient) return; + console.log('\n\ninit HttpClientNext'); + options.lookup = options.lookup ?? self.config.httpclient.lookup; + realClient = new self.HttpClientNext(self, options); + }; + return new Proxy({}, { + get(_target, prop) { + init(); + const value = realClient[prop]; + if (typeof value === 'function') { + return value.bind(realClient); + } + return value; + }, + set(_target, prop, value) { + init(); + realClient[prop] = value; + return true; + }, + has(_target, prop) { + init(); + return prop in realClient; + }, + ownKeys() { + init(); + return Reflect.ownKeys(realClient); + }, + getOwnPropertyDescriptor(_target, prop) { + init(); + return Object.getOwnPropertyDescriptor(realClient, prop); + }, + deleteProperty(_target, prop) { + init(); + return delete realClient[prop]; + }, + getPrototypeOf() { + init(); + return Object.getPrototypeOf(realClient); + }, + }); + } + /** * HttpClient instance * @see https://github.com/node-modules/urllib diff --git a/test/lib/core/httpclient.test.js b/test/lib/core/httpclient.test.js index 8bc26be14c..3004dacb01 100644 --- a/test/lib/core/httpclient.test.js +++ b/test/lib/core/httpclient.test.js @@ -382,6 +382,53 @@ describe('test/lib/core/httpclient.test.js', () => { assert(err.message.includes('url should start with http, but got unknown url')); }); }); + + it('should Proxy be fully functional', () => { + const httpclient = app.httpclient; + + // Test get trap - method access + assert(typeof httpclient.request === 'function'); + assert(typeof httpclient.curl === 'function'); + assert(typeof httpclient.safeCurl === 'function'); + + // Test has trap - 'in' operator + assert('request' in httpclient); + assert('curl' in httpclient); + assert('safeCurl' in httpclient); + + const ownKeys = Reflect.ownKeys(httpclient); + assert(ownKeys.length > 0); + + httpclient.testProp = 'test'; + const customDescriptor = Object.getOwnPropertyDescriptor(httpclient, 'testProp'); + assert(customDescriptor); + assert.equal(customDescriptor.value, 'test'); + assert.equal(customDescriptor.writable, true); + assert.equal(customDescriptor.enumerable, true); + assert.equal(customDescriptor.configurable, true); + + const proto = Object.getPrototypeOf(httpclient); + assert(proto); + assert(proto instanceof HttpclientNext); + assert(proto.constructor); + + httpclient.customProperty = 'test-value'; + assert.equal(httpclient.customProperty, 'test-value'); + + delete httpclient.customProperty; + assert.equal(httpclient.customProperty, undefined); + + delete httpclient.testProp; + assert.equal(httpclient.testProp, undefined); + + // Test that methods are properly bound + const { request } = httpclient; + assert(typeof request === 'function'); + + // Test Object.getOwnPropertyNames() which uses ownKeys trap + const propNames = Object.getOwnPropertyNames(httpclient); + assert(Array.isArray(propNames)); + }); }); describe('overwrite httpclient support allowH2=true', () => { From 32c1134f1dcea14552a9fe62e428bb9e54c3ec2d Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Mon, 12 Jan 2026 11:19:22 +0800 Subject: [PATCH 8/9] fix: test case for node < 20 --- test/lib/core/dns_resolver.test.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/lib/core/dns_resolver.test.js b/test/lib/core/dns_resolver.test.js index 84783626bd..37168891a1 100644 --- a/test/lib/core/dns_resolver.test.js +++ b/test/lib/core/dns_resolver.test.js @@ -111,7 +111,7 @@ describe('test/lib/core/dns_resolver with dns error', () => { callback(null, addresses); return; } - callback(null, [ record[0].address ], 4); + callback(null, record[0].address, 4); return; } dnsPromise.resolve4(hostname, { ttl: true }).then(addresses => { @@ -126,7 +126,7 @@ describe('test/lib/core/dns_resolver with dns error', () => { callback(null, addrList); return; } - callback(null, [ addresses[0].address ], 4); + callback(null, addresses[0].address, 4); } else { callback(new Error('no addresses found')); } @@ -137,14 +137,14 @@ describe('test/lib/core/dns_resolver with dns error', () => { }; url = server.url; url = url.replace('127.0.0.1', 'localhost'); - originalDNSServers = dns.promises.getServers(); - dns.promises.setServers([ '223.5.5.5', '223.6.6.6' ]); + originalDNSServers = dns.getServers(); + dns.setServers([ '223.5.5.5', '223.6.6.6' ]); }); afterEach(mm.restore); after(() => { - dns.promises.setServers(originalDNSServers); + dns.setServers(originalDNSServers); if (server?.server?.listening) server.server.close(); }); @@ -175,7 +175,7 @@ describe('test/lib/core/dns_resolver with dns error', () => { assert(successRes.status === 200); assert(cache.has('localhost')); // can't resolve localhost now, but cache still works - dns.promises.setServers([ '8.8.8.8' ]); + dns.setServers([ '8.8.8.8' ]); const res = await app.curl(url + '/get_headers', { dataType: 'json' }); assert(res.status === 200); From feaab271834214285e614d8e0fc2ed78b0d389dc Mon Sep 17 00:00:00 2001 From: Dipper30 Date: Mon, 12 Jan 2026 11:44:10 +0800 Subject: [PATCH 9/9] delete: console.loog --- lib/egg.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/egg.js b/lib/egg.js index ba6c5162d6..4607862af1 100644 --- a/lib/egg.js +++ b/lib/egg.js @@ -313,7 +313,6 @@ class EggApplication extends EggCore { let realClient = null; const init = () => { if (realClient) return; - console.log('\n\ninit HttpClientNext'); options.lookup = options.lookup ?? self.config.httpclient.lookup; realClient = new self.HttpClientNext(self, options); };