From 6a67a1d7e0834553f6a59321c58fedddba799d32 Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Sun, 15 Sep 2024 13:59:03 +0800 Subject: [PATCH] feat: support allowH2 on urllib@4 https://github.com/eggjs/egg/issues/5347 --- config/config.default.js | 4 +- lib/core/httpclient4.js | 38 +++++++++++++++ lib/egg.js | 6 ++- package.json | 1 + test/fixtures/apps/httpclient-allowH2/app.js | 19 ++++++++ .../config/config.default.js | 6 +++ .../apps/httpclient-allowH2/package.json | 3 ++ test/lib/core/httpclient.test.js | 48 +++++++++++++++++++ 8 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 lib/core/httpclient4.js create mode 100644 test/fixtures/apps/httpclient-allowH2/app.js create mode 100644 test/fixtures/apps/httpclient-allowH2/config/config.default.js create mode 100644 test/fixtures/apps/httpclient-allowH2/package.json diff --git a/config/config.default.js b/config/config.default.js index 5c0259ea06..49d3eee462 100644 --- a/config/config.default.js +++ b/config/config.default.js @@ -303,7 +303,8 @@ module.exports = appInfo => { * @property {Number} httpsAgent.freeSocketTimeout - httpss agent socket keepalive max free time, default is 4000 ms. * @property {Number} httpsAgent.maxSockets - https agent max socket number of one host, default is `Number.MAX_SAFE_INTEGER` @ses https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER * @property {Number} httpsAgent.maxFreeSockets - https agent max free socket number of one host, default is 256. - * @property {Boolean} useHttpClientNext - use urllib@3 HttpClient + * @property {Boolean} useHttpClientNext - use urllib@3 HttpClient, default is false + * @property {Boolean} allowH2 - use urllib@4 HttpClient and enable H2, default is false */ config.httpclient = { enableDNSCache: false, @@ -326,6 +327,7 @@ module.exports = appInfo => { maxFreeSockets: 256, }, useHttpClientNext: false, + allowH2: false, }; /** diff --git a/lib/core/httpclient4.js b/lib/core/httpclient4.js new file mode 100644 index 0000000000..7d8134f0c2 --- /dev/null +++ b/lib/core/httpclient4.js @@ -0,0 +1,38 @@ +const { HttpClient } = require('urllib4'); +const ms = require('humanize-ms'); + +class HttpClient4 extends HttpClient { + constructor(app) { + normalizeConfig(app); + const config = app.config.httpclient; + super({ + app, + defaultArgs: config.request, + allowH2: config.allowH2, + }); + this.app = app; + } + + async request(url, options) { + options = options || {}; + if (options.ctx && options.ctx.tracer) { + options.tracer = options.ctx.tracer; + } else { + options.tracer = options.tracer || this.app.tracer; + } + return await super.request(url, options); + } + + async curl(...args) { + return await this.request(...args); + } +} + +function normalizeConfig(app) { + const config = app.config.httpclient; + if (typeof config.request.timeout === 'string') { + config.request.timeout = ms(config.request.timeout); + } +} + +module.exports = HttpClient4; diff --git a/lib/egg.js b/lib/egg.js index aabba225b4..c573712736 100644 --- a/lib/egg.js +++ b/lib/egg.js @@ -14,6 +14,7 @@ const Messenger = require('./core/messenger'); const DNSCacheHttpClient = require('./core/dnscache_httpclient'); const HttpClient = require('./core/httpclient'); const HttpClientNext = require('./core/httpclient_next'); +const HttpClient4 = require('./core/httpclient4'); const createLoggers = require('./core/logger'); const Singleton = require('./core/singleton'); const utils = require('./core/utils'); @@ -51,6 +52,7 @@ class EggApplication extends EggCore { this.ContextHttpClient = ContextHttpClient; this.HttpClient = HttpClient; this.HttpClientNext = HttpClientNext; + this.HttpClient4 = HttpClient4; this.loader.loadConfig(); @@ -293,7 +295,9 @@ class EggApplication extends EggCore { */ get httpclient() { if (!this[HTTPCLIENT]) { - if (this.config.httpclient.useHttpClientNext) { + if (this.config.httpclient.allowH2) { + this[HTTPCLIENT] = new this.HttpClient4(this); + } else if (this.config.httpclient.useHttpClientNext) { this[HTTPCLIENT] = new this.HttpClientNext(this); } else if (this.config.httpclient.enableDNSCache) { this[HTTPCLIENT] = new DNSCacheHttpClient(this); diff --git a/package.json b/package.json index bbf1bf0cf3..3894e5192b 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "sendmessage": "^2.0.0", "urllib": "^2.33.0", "urllib-next": "npm:urllib@^3.22.4", + "urllib4": "npm:urllib@^4.3.0", "utility": "^2.1.0", "ylru": "^1.3.2" }, diff --git a/test/fixtures/apps/httpclient-allowH2/app.js b/test/fixtures/apps/httpclient-allowH2/app.js new file mode 100644 index 0000000000..5af2297350 --- /dev/null +++ b/test/fixtures/apps/httpclient-allowH2/app.js @@ -0,0 +1,19 @@ +const assert = require('assert'); + +module.exports = app => { + class CustomHttpClient extends app.HttpClient4 { + request(url, opt) { + return new Promise(resolve => { + assert(/^http/.test(url), 'url should start with http, but got ' + url); + resolve(); + }).then(() => { + return super.request(url, opt); + }); + } + + curl(url, opt) { + return this.request(url, opt); + } + } + app.HttpClient4 = CustomHttpClient; +}; diff --git a/test/fixtures/apps/httpclient-allowH2/config/config.default.js b/test/fixtures/apps/httpclient-allowH2/config/config.default.js new file mode 100644 index 0000000000..e477cdb9db --- /dev/null +++ b/test/fixtures/apps/httpclient-allowH2/config/config.default.js @@ -0,0 +1,6 @@ +exports.httpclient = { + allowH2: true, + request: { + timeout: 99, + }, +}; diff --git a/test/fixtures/apps/httpclient-allowH2/package.json b/test/fixtures/apps/httpclient-allowH2/package.json new file mode 100644 index 0000000000..56b5d28da8 --- /dev/null +++ b/test/fixtures/apps/httpclient-allowH2/package.json @@ -0,0 +1,3 @@ +{ + "name": "httpclient-overwrite" +} diff --git a/test/lib/core/httpclient.test.js b/test/lib/core/httpclient.test.js index 2b5fbb73e5..4b4f8e2744 100644 --- a/test/lib/core/httpclient.test.js +++ b/test/lib/core/httpclient.test.js @@ -255,6 +255,54 @@ describe('test/lib/core/httpclient.test.js', () => { }); }); + describe('overwrite httpclient support allowH2=true', () => { + let app; + before(() => { + app = utils.app('apps/httpclient-allowH2'); + return app.ready(); + }); + after(() => app.close()); + + it('should set request default global timeout to 99ms', async () => { + await assert.rejects(async () => { + await app.httpclient.curl(`${url}/timeout`); + }, err => { + assert.equal(err.name, 'HttpClientRequestTimeoutError'); + assert(err.message.includes('Request timeout for 99 ms')); + return true; + }); + }); + + it('should request http1.1 success', async () => { + const result = await app.httpclient.curl(`${url}`, { + dataType: 'text', + }); + assert.equal(result.status, 200); + assert.equal(result.data, 'GET /'); + }); + + it('should request http2 success', async () => { + for (let i = 0; i < 10; i++) { + const result = await app.httpclient.curl('https://registry.npmmirror.com', { + dataType: 'json', + timeout: 5000, + }); + assert.equal(result.status, 200); + assert.equal(result.headers['content-type'], 'application/json; charset=utf-8'); + assert.equal(result.data.sync_model, 'all'); + } + }); + + it('should assert url', async () => { + await assert.rejects(async () => { + await app.httpclient.curl('unknown url'); + }, err => { + assert.match(err.message, /url should start with http, but got unknown url/); + return true; + }); + }); + }); + describe('httpclient tracer', () => { let app; before(() => {