Skip to content

Commit 715f67a

Browse files
authored
feat: enable custom lookup (eggjs#5751)
pick from eggjs#5749 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **Bug Fixes** * HttpClient initialization now gracefully handles missing configuration, preventing potential runtime errors. * **New Features** * Added support for custom DNS lookup function configuration, allowing users to override default DNS resolution behavior. * **Tests** * Expanded test coverage for DNS caching functionality and HTTP client DNS resolution scenarios. <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 9eeb060 commit 715f67a

File tree

5 files changed

+119
-201
lines changed

5 files changed

+119
-201
lines changed

packages/egg/src/lib/core/httpclient.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export class HttpClient extends RawHttpClient {
2525

2626
constructor(app: EggApplicationCore, options: HttpClientOptions = {}) {
2727
normalizeConfig(app);
28-
const config = app.config.httpclient;
28+
const config = app.config.httpclient || {};
29+
options.lookup = options.lookup ?? config.lookup;
2930
const initOptions: HttpClientOptions = {
3031
...options,
3132
defaultArgs: {

packages/egg/src/lib/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Socket } from 'node:net';
1+
import type { Socket, LookupFunction } from 'node:net';
22

33
import type { FileLoaderOptions, EggAppConfig as EggCoreAppConfig, EggAppInfo } from '@eggjs/core';
44
import type { EggLoggerOptions, EggLoggersOptions } from 'egg-logger';
@@ -66,6 +66,7 @@ export interface HttpClientConfig {
6666
* Allow http2
6767
*/
6868
allowH2?: boolean;
69+
lookup?: LookupFunction;
6970
}
7071

7172
/**
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,24 @@
11
'use strict';
22

33
exports.httpclient = {
4-
enableDNSCache: true,
5-
dnsCacheLookupInterval: 5000,
4+
lookup: function (hostname, options, callback) {
5+
const IP_REGEX = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
6+
if (IP_REGEX.test(hostname)) {
7+
const family = typeof options.family === 'number' ? options.family : 4;
8+
if (options.all) {
9+
callback(null, [{ address: hostname, family }]);
10+
} else {
11+
callback(null, hostname, family);
12+
}
13+
} else {
14+
const resultIp = '127.0.0.1';
15+
if (options.all) {
16+
callback(null, [{ address: resultIp, family: 4 }]);
17+
} else {
18+
callback(null, resultIp, 4);
19+
}
20+
}
21+
},
622
};
723

824
exports.keys = 'test key';
Lines changed: 59 additions & 197 deletions
Original file line numberDiff line numberDiff line change
@@ -1,199 +1,61 @@
1-
import { describe } from 'vitest';
2-
// import { strict as assert } from 'node:assert';
3-
// import dns from 'node:dns/promises';
4-
// import { parse as urlparse } from 'node:url';
5-
// import { mm } from '@eggjs/mock';
6-
// import { createApp, type MockApplication, startLocalServer } from '../../utils.js';
1+
import { strict as assert } from 'node:assert';
2+
import http from 'node:http';
73

8-
describe.skip('test/lib/core/dnscache_httpclient.test.ts', () => {
9-
// let app: MockApplication;
10-
// let url: string;
11-
// let host: string;
12-
// let originalDNSServers: string[];
13-
// before(async () => {
14-
// app = createApp('apps/dnscache_httpclient');
15-
// await app.ready();
16-
// url = await startLocalServer();
17-
// url = url.replace('127.0.0.1', 'localhost');
18-
// host = urlparse(url).host!;
19-
// originalDNSServers = dns.getServers();
20-
// });
21-
// afterEach(mm.restore);
22-
// afterEach(() => {
23-
// // After trying to set Server Ips forcedly,
24-
// // try to restore them to usual ones
25-
// dns.setServers(originalDNSServers);
26-
// });
27-
// it('should ctx.curl work and set host', async () => {
28-
// await app.httpRequest()
29-
// .get('/?url=' + encodeURIComponent(url + '/get_headers'))
30-
// .expect(200)
31-
// .expect(/"host":"localhost:\d+"/);
32-
// await app.httpRequest()
33-
// .get('/?url=' + encodeURIComponent(url + '/get_headers') + '&host=localhost.foo.com')
34-
// .expect(200)
35-
// .expect(/"host":"localhost\.foo\.com"/);
36-
// await app.httpRequest()
37-
// .get('/?url=' + encodeURIComponent(url + '/get_headers') + '&Host=localhost2.foo.com')
38-
// .expect(200)
39-
// .expect(/"host":"localhost2\.foo\.com"/);
40-
// });
41-
// /**
42-
// * This test failure can be totally ignored because it depends on how your service provider
43-
// * deals with the domain when you cannot find that:Some providers will batchly switch
44-
// * those invalid domains to a certain server. So you can still find the fixed IP by
45-
// * calling `dns.lookup()`.
46-
// *
47-
// * To make sure that your domain exists or not, just use `ping your_domain_here` instead.
48-
// */
49-
// it('should throw error when the first dns lookup fail', async () => {
50-
// if (!process.env.CI) {
51-
// // Avoid Network service provider DNS pollution
52-
// // alidns http://www.alidns.com/node-distribution/
53-
// // Not sure it will work for all servers
54-
// dns.setServers([
55-
// '223.5.5.5',
56-
// '223.6.6.6',
57-
// ]);
58-
// }
59-
// await app.httpRequest()
60-
// .get('/?url=' + encodeURIComponent('http://notexists-1111111local-domain.com'))
61-
// .expect(500)
62-
// .expect(/getaddrinfo ENOTFOUND notexists-1111111local-domain\.com/);
63-
// });
64-
// it('should use local cache dns result when dns lookup error', async () => {
65-
// await app.httpRequest()
66-
// .get('/?url=' + encodeURIComponent(url + '/get_headers'))
67-
// .expect(200)
68-
// .expect(/"host":"localhost:\d+"/);
69-
// // mock local cache expires and mock dns lookup throw error
70-
// app.httpclient.dnsCache.get('localhost').timestamp = 0;
71-
// mm.error(dns, 'lookup', 'mock dns lookup error');
72-
// await app.httpRequest()
73-
// .get('/?url=' + encodeURIComponent(url + '/get_headers'))
74-
// .expect(200)
75-
// .expect(/"host":"localhost:\d+"/);
76-
// });
77-
// it('should app.curl work', async () => {
78-
// const result = await app.curl(url + '/get_headers', { dataType: 'json' });
79-
// assert(result.status === 200);
80-
// assert(result.data.host === host);
81-
// const result2 = await app.httpclient.curl(url + '/get_headers', { dataType: 'json' });
82-
// assert(result2.status === 200);
83-
// assert(result2.data.host === host);
84-
// });
85-
// it('should app.curl work on lookup error', async () => {
86-
// const result = await app.curl(url + '/get_headers', { dataType: 'json' });
87-
// assert(result.status === 200);
88-
// assert(result.data.host === host);
89-
// // mock local cache expires and mock dns lookup throw error
90-
// app.httpclient.dnsCache.get('localhost').timestamp = 0;
91-
// mm.error(dns, 'lookup', 'mock dns lookup error');
92-
// const result2 = await app.httpclient.curl(url + '/get_headers', { dataType: 'json' });
93-
// assert(result2.status === 200);
94-
// assert(result2.data.host === host);
95-
// });
96-
// it('should app.curl(obj)', async () => {
97-
// const obj = urlparse(url + '/get_headers');
98-
// const result = await app.curl(obj, { dataType: 'json' });
99-
// assert(result.status === 200);
100-
// assert(result.data.host === host);
101-
// const obj2 = urlparse(url + '/get_headers');
102-
// // mock obj2.host
103-
// obj2.host = null;
104-
// const result2 = await app.curl(obj2, { dataType: 'json' });
105-
// assert(result2.status === 200);
106-
// assert(result2.data.host === host);
107-
// });
108-
// it('should dnsCacheMaxLength work', async () => {
109-
// mm(dns, 'lookup', async () => {
110-
// return { address: '127.0.0.1', family: 4 };
111-
// });
112-
// // reset lru cache
113-
// mm(app.httpclient.dnsCache, 'max', 1);
114-
// mm(app.httpclient.dnsCache, 'size', 0);
115-
// mm(app.httpclient.dnsCache, 'cache', new Map());
116-
// mm(app.httpclient.dnsCache, '_cache', new Map());
117-
// let obj = urlparse(url + '/get_headers');
118-
// let result = await app.curl(obj, { dataType: 'json' });
119-
// assert(result.status === 200);
120-
// assert(result.data.host === host);
121-
// assert(app.httpclient.dnsCache.get('localhost'));
122-
// obj = urlparse(url.replace('localhost', 'another.com') + '/get_headers');
123-
// result = await app.curl(obj, { dataType: 'json' });
124-
// assert(result.status === 200);
125-
// assert(result.data.host === obj.host);
126-
// assert(!app.httpclient.dnsCache.get('localhost'));
127-
// assert(app.httpclient.dnsCache.get('another.com'));
128-
// });
129-
// it('should cache and update', async () => {
130-
// mm(dns, 'lookup', async () => {
131-
// return { address: '127.0.0.1', family: 4 };
132-
// });
133-
// let obj = urlparse(url + '/get_headers');
134-
// let result = await app.curl(obj, { dataType: 'json' });
135-
// assert(result.status === 200);
136-
// assert(result.data.host === host);
137-
// let record = app.httpclient.dnsCache.get('localhost');
138-
// const timestamp = record.timestamp;
139-
// assert(record);
140-
// obj = urlparse(url + '/get_headers');
141-
// result = await app.curl(obj, { dataType: 'json' });
142-
// assert(result.status === 200);
143-
// assert(result.data.host === host);
144-
// record = app.httpclient.dnsCache.get('localhost');
145-
// assert(timestamp === record.timestamp);
146-
// await scheduler.wait(5500);
147-
// obj = urlparse(url + '/get_headers');
148-
// result = await app.curl(obj, { dataType: 'json' });
149-
// assert(result.status === 200);
150-
// assert(result.data.host === host);
151-
// record = app.httpclient.dnsCache.get('localhost');
152-
// assert(timestamp !== record.timestamp);
153-
// });
154-
// it('should cache and update with agent', async () => {
155-
// const agent = app._agent;
156-
// mm(dns, 'lookup', async () => {
157-
// return { address: '127.0.0.1', family: 4 };
158-
// });
159-
// let obj = urlparse(url + '/get_headers');
160-
// let result = await agent.curl(obj, { dataType: 'json' });
161-
// assert(result.status === 200);
162-
// assert(result.data.host === host);
163-
// let record = agent.httpclient.dnsCache.get('localhost');
164-
// const timestamp = record.timestamp;
165-
// assert(record);
166-
// obj = urlparse(url + '/get_headers');
167-
// result = await agent.curl(obj, { dataType: 'json' });
168-
// assert(result.status === 200);
169-
// assert(result.data.host === host);
170-
// record = agent.httpclient.dnsCache.get('localhost');
171-
// assert(timestamp === record.timestamp);
172-
// await scheduler.wait(5500);
173-
// obj = urlparse(url + '/get_headers');
174-
// result = await agent.curl(obj, { dataType: 'json' });
175-
// assert(result.status === 200);
176-
// assert(result.data.host === host);
177-
// record = agent.httpclient.dnsCache.get('localhost');
178-
// assert(timestamp !== record.timestamp);
179-
// });
180-
// it('should not cache ip', async () => {
181-
// const obj = urlparse(url.replace('localhost', '127.0.0.1') + '/get_headers');
182-
// const result = await app.curl(obj, { dataType: 'json' });
183-
// assert(result.status === 200);
184-
// assert(result.data.host === obj.host);
185-
// assert(!app.httpclient.dnsCache.get('127.0.0.1'));
186-
// });
187-
// describe('disable DNSCache in one request', () => {
188-
// beforeEach(() => {
189-
// mm(app.httpclient.dnsCache, 'size', 0);
190-
// });
191-
// it('should work', async () => {
192-
// await app.httpRequest()
193-
// .get('/?disableDNSCache=true&url=' + encodeURIComponent(url + '/get_headers'))
194-
// .expect(200)
195-
// .expect(/"host":"localhost:\d+"/);
196-
// assert(app.httpclient.dnsCache.size === 0);
197-
// });
198-
// });
4+
import { describe, it, beforeAll, afterAll } from 'vitest';
5+
6+
import { createApp, type MockApplication, startNewLocalServer } from '../../utils.js';
7+
8+
describe('test/lib/core/dnscache_httpclient.test.ts', () => {
9+
let app: MockApplication;
10+
let url: string;
11+
let serverInfo: { url: string; server: http.Server };
12+
13+
beforeAll(async () => {
14+
app = createApp('apps/dnscache_httpclient');
15+
await app.ready();
16+
serverInfo = await startNewLocalServer();
17+
url = serverInfo.url.replace('127.0.0.1', 'custom-localhost');
18+
});
19+
20+
afterAll(() => {
21+
if (serverInfo?.server?.listening) {
22+
serverInfo.server.close();
23+
}
24+
});
25+
26+
it('should bypass dns resolve', async () => {
27+
// will not resolve ip
28+
const res = await app.curl(serverInfo.url + '/get_headers', { dataType: 'json' });
29+
assert(res.status === 200);
30+
});
31+
32+
it('should curl work', async () => {
33+
// will resolve custom-localhost to 127.0.0.1
34+
const res = await app.curl(url + '/get_headers', { dataType: 'json' });
35+
assert(res.status === 200);
36+
});
37+
38+
it('should safeCurl also work', async () => {
39+
// will resolve custom-localhost to 127.0.0.1
40+
const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
41+
assert(res.status === 200);
42+
});
43+
44+
it('should safeCurl also work', async () => {
45+
// will resolve custom-localhost to 127.0.0.1
46+
const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
47+
assert(res.status === 200);
48+
});
49+
50+
it('should server down and catch error', async () => {
51+
try {
52+
if (serverInfo?.server?.listening) await serverInfo.server.close();
53+
// will resolve to 127.0.0.1
54+
const res = await app.curl(url + '/get_headers', { dataType: 'json' });
55+
assert(res.status !== 200);
56+
} catch (err: any) {
57+
assert(err);
58+
assert(err.message.includes('ECONNREFUSED'));
59+
}
60+
});
19961
});

packages/egg/test/utils.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,41 @@ function formatOptions(name: string | MockOptions, options?: MockOptions) {
153153
...options,
154154
};
155155
}
156+
157+
export async function startNewLocalServer(ip = '127.0.0.1'): Promise<{
158+
url: string;
159+
server: http.Server;
160+
}> {
161+
let localServer: http.Server;
162+
return new Promise((resolve, reject) => {
163+
const app = new Koa();
164+
app.use(async (ctx) => {
165+
if (ctx.path === '/get_headers') {
166+
ctx.body = {
167+
headers: ctx.request.headers,
168+
host: ctx.request.headers.host,
169+
};
170+
return;
171+
}
172+
ctx.body = JSON.stringify(`${ctx.method} ${ctx.path}`);
173+
});
174+
localServer = http.createServer(app.callback());
175+
const serverCallback = () => {
176+
const addressRes = localServer.address();
177+
const port = addressRes && typeof addressRes === 'object' ? addressRes.port : addressRes;
178+
const url = `http://${ip}:` + port;
179+
return resolve({ url, server: localServer });
180+
};
181+
localServer.listen(0, serverCallback);
182+
localServer.on('error', (e: any) => {
183+
if (e.code === 'EADDRINUSE') {
184+
setTimeout(() => {
185+
localServer.close();
186+
localServer.listen(0, serverCallback);
187+
}, 1000);
188+
} else {
189+
reject(e);
190+
}
191+
});
192+
});
193+
}

0 commit comments

Comments
 (0)