egg/test/lib/core/httpclient.test.js
fengmk2 e4f7069904
feat: use urllib@4.5.0 (#5371)
auto fallback to urllib@3 when require urllib4 error

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
	- Expanded CI workflow to include Node.js version 23 for testing.
- Enhanced `HttpClientNext` class with improved error handling and
configuration management.
- Added support for HTTP/2 functionality in the test suite, including
self-signed certificate handling.

- **Bug Fixes**
- Improved error handling for library imports based on Node.js versions.

- **Chores**
	- Updated dependency versions in `package.json`. 
- Modified HTTP client configuration to include a new `connect`
property.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2024-11-30 21:35:55 +08:00

801 lines
22 KiB
JavaScript

const assert = require('node:assert');
const { once } = require('node:events');
const { createSecureServer } = require('node:http2');
const mm = require('egg-mock');
const urllib = require('urllib');
const pem = require('https-pem');
const Httpclient = require('../../../lib/core/httpclient');
const HttpclientNext = require('../../../lib/core/httpclient_next');
const utils = require('../../utils');
describe('test/lib/core/httpclient.test.js', () => {
let client;
let clientNext;
let url;
before(() => {
client = new Httpclient({
deprecate: () => {},
config: {
httpclient: {
request: {},
httpAgent: {},
httpsAgent: {},
},
},
});
client.on('request', info => {
info.args.headers = info.args.headers || {};
info.args.headers['mock-traceid'] = 'mock-traceid';
info.args.headers['mock-rpcid'] = 'mock-rpcid';
});
clientNext = new HttpclientNext({
config: {
httpclient: {
request: {},
},
},
});
clientNext.on('request', info => {
info.args.headers = info.args.headers || {};
info.args.headers['mock-traceid'] = 'mock-traceid';
info.args.headers['mock-rpcid'] = 'mock-rpcid';
});
});
before(async () => {
url = await utils.startLocalServer();
});
afterEach(mm.restore);
it('should request ok with log', done => {
const args = {
dataType: 'text',
};
client.once('response', info => {
assert(info.req.options.headers['mock-traceid'] === 'mock-traceid');
assert(info.req.options.headers['mock-rpcid'] === 'mock-rpcid');
done();
});
client.request(url, args);
});
it('should curl ok with log', done => {
const args = {
dataType: 'text',
};
client.once('response', info => {
assert(info.req.options.headers['mock-traceid'] === 'mock-traceid');
assert(info.req.options.headers['mock-rpcid'] === 'mock-rpcid');
done();
});
client.curl(url, args);
});
it('should mock ENETUNREACH error', async () => {
mm(urllib.HttpClient2.prototype, 'request', () => {
const err = new Error('connect ENETUNREACH 1.1.1.1:80 - Local (127.0.0.1)');
err.code = 'ENETUNREACH';
return Promise.reject(err);
});
await assert.rejects(async () => {
await client.request(url);
}, err => {
assert(err.name === 'HttpClientError');
assert(err.code === 'httpclient_ENETUNREACH');
assert(err.message === 'connect ENETUNREACH 1.1.1.1:80 - Local (127.0.0.1) [ https://eggjs.org/zh-cn/faq/httpclient_ENETUNREACH ]');
return true;
});
});
it('should handle timeout error', async () => {
await assert.rejects(async () => {
await client.request(url + '/timeout', { timeout: 100 });
}, err => {
assert(err.name === 'ResponseTimeoutError');
return true;
});
});
it('should support safeCurl', async () => {
let ip;
let family;
let host;
mm(client.app.config, 'security', {
ssrf: {
checkAddress(aIp, aFamilay, aHost) {
ip = aIp;
family = aFamilay;
host = aHost;
return true;
},
},
});
await client.safeCurl(url);
assert(ip);
assert(family);
assert(host);
});
describe('HttpClientNext', () => {
it('should request ok with log', async () => {
const args = {
dataType: 'text',
};
let info;
clientNext.once('response', meta => {
info = meta;
});
const { status } = await clientNext.request(url, args);
assert(status === 200);
assert(info.req.options.headers['mock-traceid'] === 'mock-traceid');
assert(info.req.options.headers['mock-rpcid'] === 'mock-rpcid');
assert(info.req.args.headers['mock-traceid'] === 'mock-traceid');
assert(info.req.args.headers['mock-rpcid'] === 'mock-rpcid');
});
it('should curl ok with log', async () => {
const args = {
dataType: 'text',
};
let info;
clientNext.once('response', meta => {
info = meta;
});
const { status } = await clientNext.curl(url, args);
assert(status === 200);
assert(info.req.options.headers['mock-traceid'] === 'mock-traceid');
assert(info.req.options.headers['mock-rpcid'] === 'mock-rpcid');
assert(info.req.args.headers['mock-traceid'] === 'mock-traceid');
assert(info.req.args.headers['mock-rpcid'] === 'mock-rpcid');
});
it('should request with error', async () => {
await assert.rejects(async () => {
const response = await clientNext.request(url + '/error', {
dataType: 'json',
});
console.log(response);
}, err => {
assert.equal(err.name, 'JSONResponseFormatError');
assert.match(err.message, /this is an error/);
assert(err.res);
assert.equal(err.res.status, 500);
return true;
});
});
it('should support safeCurl', async () => {
let ip;
let family;
let host;
mm(clientNext.app.config, 'security', {
ssrf: {
checkAddress(aIp, aFamilay, aHost) {
ip = aIp;
family = aFamilay;
host = aHost;
return true;
},
},
});
await clientNext.safeCurl(url);
assert(ip);
assert(family);
assert(host);
});
});
describe('httpclient.httpAgent.timeout < 30000', () => {
let app;
before(() => {
app = utils.app('apps/httpclient-agent-timeout-3000');
return app.ready();
});
after(() => app.close());
it('should auto reset httpAgent.timeout to 30000', () => {
// should access httpclient first
assert(app.httpclient);
assert(app.config.httpclient.timeout === 3000);
assert(app.config.httpclient.httpAgent.timeout === 30000);
assert(app.config.httpclient.httpsAgent.timeout === 30000);
});
it('should set request default global timeout to 10s', () => {
// should access httpclient first
assert(app.httpclient);
assert(app.config.httpclient.request.timeout === 10000);
});
it('should convert compatibility options to agent options', () => {
// should access httpclient first
assert(app.httpclient);
assert(app.config.httpclient.httpAgent.freeSocketTimeout === 2000);
assert(app.config.httpclient.httpsAgent.freeSocketTimeout === 2000);
assert(app.config.httpclient.httpAgent.maxSockets === 100);
assert(app.config.httpclient.httpsAgent.maxSockets === 100);
assert(app.config.httpclient.httpAgent.maxFreeSockets === 100);
assert(app.config.httpclient.httpsAgent.maxFreeSockets === 100);
assert(app.config.httpclient.httpAgent.keepAlive === false);
assert(app.config.httpclient.httpsAgent.keepAlive === false);
});
});
describe('httpclient.request.timeout = 100', () => {
let app;
before(() => {
app = utils.app('apps/httpclient-request-timeout-100');
return app.ready();
});
after(() => app.close());
it('should set request default global timeout to 100ms', () => {
return app.httpclient.curl(`${url}/timeout`)
.catch(err => {
assert(err);
assert(err.name === 'ResponseTimeoutError');
assert(err.message.includes('Response timeout for 100ms'));
});
});
});
describe('overwrite httpclient', () => {
let app;
before(() => {
app = utils.app('apps/httpclient-overwrite');
return app.ready();
});
after(() => app.close());
it('should set request default global timeout to 100ms', () => {
return app.httpclient.curl(`${url}/timeout`)
.catch(err => {
assert(err);
assert(err.name === 'ResponseTimeoutError');
assert(err.message.includes('Response timeout for 100ms'));
});
});
it('should assert url', () => {
return app.httpclient.curl('unknown url')
.catch(err => {
assert(err);
assert(err.message.includes('url should start with http, but got unknown url'));
});
});
});
describe('overwrite httpclient support useHttpClientNext=true', () => {
let app;
before(() => {
app = utils.app('apps/httpclient-next-overwrite');
return app.ready();
});
after(() => app.close());
it('should work', async () => {
const res = await app.httpclient.request(url);
assert.equal(res.status, 200);
assert.equal(res.data.toString(), 'GET /');
});
it('should set request default global timeout to 99ms', () => {
return app.httpclient.curl(`${url}/timeout`)
.catch(err => {
assert(err);
assert(err.name === 'HttpClientRequestTimeoutError');
assert(err.message.includes('Request timeout for 99 ms'));
});
});
it('should assert url', () => {
return app.httpclient.curl('unknown url')
.catch(err => {
assert(err);
assert(err.message.includes('url should start with http, but got unknown url'));
});
});
});
describe('overwrite httpclient support allowH2=true', () => {
let app;
before(() => {
app = utils.app('apps/httpclient-allowH2');
return app.ready();
});
after(() => app.close());
it('should work on http2', async () => {
const res = await app.httpclient.request(url, {
timeout: 5000,
});
assert.equal(res.status, 200);
assert.equal(res.data.toString(), 'GET /');
// assert.equal(sensitiveHeaders in res.headers, false);
const res2 = await app.httpclient.request('https://registry.npmmirror.com/urllib/latest', {
dataType: 'json',
timeout: 5000,
});
assert.equal(res2.status, 200);
assert.equal(res2.data.name, 'urllib');
// assert.equal(sensitiveHeaders in res2.headers, true);
});
it('should work on http2.server with self-signed certificate', async () => {
const server = createSecureServer(pem);
server.on('stream', (stream, headers) => {
assert.equal(headers[':method'], 'GET');
stream.respond({
'content-type': 'text/plain; charset=utf-8',
'x-custom-h2': 'hello',
':status': 200,
});
stream.end('hello h2!');
// console.log(headers);
const mainNodejsVersion = parseInt(process.versions.node.split('.')[0]);
if (mainNodejsVersion >= 18) {
assert.match(headers['user-agent'], /node\-urllib\/4\.\d+\.\d+/);
} else {
assert.match(headers['user-agent'], /node\-urllib\/3\.\d+\.\d+/);
}
});
server.listen(0);
await once(server, 'listening');
const response = await app.httpclient.request(`https://localhost:${server.address().port}`, {
dataType: 'text',
headers: {
'x-my-header': 'foo',
},
});
assert.equal(response.status, 200);
assert.equal(response.headers['x-custom-h2'], 'hello');
assert.equal(response.data, 'hello h2!');
server.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.match(err.message, /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(() => {
app = utils.app('apps/httpclient-tracer');
return app.ready();
});
after(() => app.close());
it('should app request auto set tracer', async () => {
const httpclient = app.httpclient;
// httpClient alias to httpclient
assert(app.httpClient);
assert.equal(app.httpClient, app.httpclient);
let reqTracer;
let resTracer;
httpclient.on('request', function(options) {
reqTracer = options.args.tracer;
});
httpclient.on('response', function(options) {
resTracer = options.req.args.tracer;
});
let res = await httpclient.request(url, {
method: 'GET',
});
assert(res.status === 200);
assert(reqTracer === resTracer);
assert(reqTracer.traceId);
assert(reqTracer.traceId === resTracer.traceId);
reqTracer = null;
resTracer = null;
res = await httpclient.request(url);
assert(res.status === 200);
assert(reqTracer === resTracer);
assert(reqTracer.traceId);
assert(reqTracer.traceId === resTracer.traceId);
});
it('should agent request auto set tracer', async () => {
const httpclient = app.agent.httpclient;
let reqTracer;
let resTracer;
httpclient.on('request', function(options) {
reqTracer = options.args.tracer;
});
httpclient.on('response', function(options) {
resTracer = options.req.args.tracer;
});
const res = await httpclient.request(url, {
method: 'GET',
});
assert(res.status === 200);
assert(reqTracer === resTracer);
assert(reqTracer.traceId);
assert(reqTracer.traceId === resTracer.traceId);
});
it('should app request with ctx and tracer', async () => {
const httpclient = app.httpclient;
let reqTracer;
let resTracer;
httpclient.on('request', function(options) {
reqTracer = options.args.tracer;
});
httpclient.on('response', function(options) {
resTracer = options.req.args.tracer;
});
let res = await httpclient.request(url, {
method: 'GET',
});
assert(res.status === 200);
assert(reqTracer.traceId);
assert(reqTracer.traceId === resTracer.traceId);
reqTracer = null;
resTracer = null;
res = await httpclient.request(url, {
method: 'GET',
ctx: {},
tracer: {
id: '1234',
},
});
assert(res.status === 200);
assert(reqTracer.id === resTracer.id);
assert(reqTracer.id === '1234');
reqTracer = null;
resTracer = null;
res = await httpclient.request(url, {
method: 'GET',
ctx: {
tracer: {
id: '5678',
},
},
});
assert(res.status === 200);
assert(reqTracer.id === resTracer.id);
assert(reqTracer.id === '5678');
});
});
describe('httpclient next with tracer', () => {
let app;
before(() => {
app = utils.app('apps/httpclient-next-with-tracer');
return app.ready();
});
after(() => app.close());
it('should app request auto set tracer', async () => {
const httpclient = app.httpclient;
let reqTracer;
let resTracer;
httpclient.on('request', function(options) {
reqTracer = options.args.tracer;
});
httpclient.on('response', function(options) {
resTracer = options.req.args.tracer;
});
const opaque = { now: Date.now() };
let res = await httpclient.request(url, {
method: 'GET',
dataType: 'text',
opaque,
});
assert(res.opaque === opaque);
assert(res.status === 200);
assert(reqTracer);
assert(resTracer);
assert(reqTracer === resTracer);
assert(reqTracer.traceId);
assert(reqTracer.traceId === resTracer.traceId);
reqTracer = null;
resTracer = null;
res = await httpclient.request(url);
assert(res.status === 200);
assert(reqTracer === resTracer);
assert(reqTracer.traceId);
assert(reqTracer.traceId === resTracer.traceId);
});
it('should agent request auto set tracer', async () => {
const httpclient = app.agent.httpclient;
let reqTracer;
let resTracer;
httpclient.on('request', function(options) {
reqTracer = options.args.tracer;
});
httpclient.on('response', function(options) {
resTracer = options.req.args.tracer;
});
const res = await httpclient.request(url, {
method: 'GET',
});
assert(res.status === 200);
assert(reqTracer === resTracer);
assert(reqTracer.traceId);
assert(reqTracer.traceId === resTracer.traceId);
});
it('should app request with ctx and tracer', async () => {
const httpclient = app.httpclient;
let reqTracer;
let resTracer;
httpclient.on('request', function(options) {
reqTracer = options.args.tracer;
});
httpclient.on('response', function(options) {
resTracer = options.req.args.tracer;
});
let res = await httpclient.request(url, {
method: 'GET',
});
assert(res.status === 200);
assert(reqTracer.traceId);
assert(reqTracer.traceId === resTracer.traceId);
reqTracer = null;
resTracer = null;
res = await httpclient.request(url, {
method: 'GET',
ctx: {},
tracer: {
id: '1234',
},
});
assert(res.status === 200);
assert(reqTracer.id === resTracer.id);
assert(reqTracer.id === '1234');
reqTracer = null;
resTracer = null;
res = await httpclient.request(url, {
method: 'GET',
ctx: {
tracer: {
id: '5678',
},
},
});
assert(res.status === 200);
assert(reqTracer.id === resTracer.id);
assert(reqTracer.id === '5678');
});
});
describe('before app ready multi httpclient request tracer', () => {
let app;
before(() => {
app = utils.app('apps/httpclient-tracer');
return app.ready();
});
after(() => app.close());
it('should app request before ready use same tracer', async () => {
const httpclient = app.httpclient;
const reqTracers = [];
const resTracers = [];
httpclient.on('request', function(options) {
reqTracers.push(options.args.tracer);
});
httpclient.on('response', function(options) {
resTracers.push(options.req.args.tracer);
});
let res = await httpclient.request(url, {
method: 'GET',
timeout: 20000,
});
assert(res.status === 200);
res = await httpclient.request('https://registry.npmmirror.com', {
method: 'GET',
timeout: 20000,
});
assert(res.status === 200);
res = await httpclient.request('https://npmmirror.com', {
method: 'GET',
timeout: 20000,
});
assert(res.status === 200);
assert(reqTracers.length === 3);
assert(resTracers.length === 3);
assert(reqTracers[0] !== reqTracers[1]);
assert(reqTracers[1] !== reqTracers[2]);
assert(resTracers[0] !== reqTracers[2]);
assert(resTracers[1] !== resTracers[0]);
assert(resTracers[2] !== resTracers[1]);
assert(reqTracers[0].traceId);
});
});
describe('compatibility freeSocketKeepAliveTimeout', () => {
it('should convert freeSocketKeepAliveTimeout to freeSocketTimeout', () => {
let mockApp = {
config: {
httpclient: {
request: {},
freeSocketKeepAliveTimeout: 1000,
httpAgent: {},
httpsAgent: {},
},
},
};
let client = new Httpclient(mockApp);
assert(client);
assert(mockApp.config.httpclient.freeSocketTimeout === 1000);
assert(!mockApp.config.httpclient.freeSocketKeepAliveTimeout);
assert(mockApp.config.httpclient.httpAgent.freeSocketTimeout === 1000);
assert(mockApp.config.httpclient.httpsAgent.freeSocketTimeout === 1000);
mockApp = {
config: {
httpclient: {
request: {},
httpAgent: {
freeSocketKeepAliveTimeout: 1001,
},
httpsAgent: {
freeSocketKeepAliveTimeout: 1002,
},
},
},
};
client = new Httpclient(mockApp);
assert(client);
assert(mockApp.config.httpclient.httpAgent.freeSocketTimeout === 1001);
assert(!mockApp.config.httpclient.httpAgent.freeSocketKeepAliveTimeout);
assert(mockApp.config.httpclient.httpsAgent.freeSocketTimeout === 1002);
assert(!mockApp.config.httpclient.httpsAgent.freeSocketKeepAliveTimeout);
});
});
describe('httpclient retry', () => {
let app;
before(() => {
app = utils.app('apps/httpclient-retry');
return app.ready();
});
after(() => app.close());
it('should retry when httpclient fail', async () => {
let hasRetry = false;
const res = await app.httpclient.curl(`${url}/retry`, {
retry: 1,
retryDelay: 100,
isRetry(res) {
const shouldRetry = res.status >= 500;
if (shouldRetry) {
hasRetry = true;
}
return shouldRetry;
},
});
assert(hasRetry);
assert(res.status === 200);
});
it('should retry when httpclient fail', async () => {
let hasRetry = false;
const res = await app.httpclient.curl(`${url}/retry`, {
retry: 1,
retryDelay: 100,
isRetry(res) {
const shouldRetry = res.status >= 500;
if (shouldRetry) {
hasRetry = true;
}
return shouldRetry;
},
});
assert(hasRetry);
assert(res.status === 200);
});
});
});