Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions app/extend/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const LOCALS_LIST = Symbol('Context#localsList');
const COOKIES = Symbol('Context#cookies');
const CONTEXT_LOGGERS = Symbol('Context#logger');
const CONTEXT_HTTPCLIENT = Symbol('Context#httpclient');
const CONTEXT_FETCH = Symbol('Context#fetch');
const CONTEXT_ROUTER = Symbol('Context#router');

const proto = module.exports = {
Expand Down Expand Up @@ -49,6 +50,51 @@ const proto = module.exports = {
return this.httpclient.curl(url, options);
},

/**
* Get a wrapper fetch instance with context
*
* @return {ContextFetch} the wrapper fetch instance
*/
get fetchClient() {
if (!this.app.fetch) return null;
if (!this[CONTEXT_FETCH]) {
this[CONTEXT_FETCH] = new this.app.ContextFetch(this);
}
return this[CONTEXT_FETCH];
},

/**
* Shortcut for fetchClient.fetch
*
* @function Context#fetch
* @param {String|URL} url - request url address.
* @param {Object} [init] - fetch init options.
* @return {Promise<Response>} see {@link ContextFetch#fetch}
*/
fetch(url, init) {
const client = this.fetchClient;
if (!client) {
throw new Error('fetch is not available, please upgrade to Node.js >= 20 and install urllib4');
}
return client.fetch(url, init);
},

/**
* Shortcut for fetchClient.safeFetch with SSRF protection
*
* @function Context#safeFetch
* @param {String|URL} url - request url address.
* @param {Object} [init] - fetch init options.
* @return {Promise<Response>} see {@link ContextFetch#safeFetch}
*/
safeFetch(url, init) {
const client = this.fetchClient;
if (!client) {
throw new Error('safeFetch is not available, please upgrade to Node.js >= 20 and install urllib4');
}
return client.safeFetch(url, init);
},

/**
* Alias to {@link Application#router}
*
Expand Down
21 changes: 18 additions & 3 deletions config/config.default.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,14 @@ module.exports = appInfo => {
/**
* The option for httpclient
* @member Config#httpclient
* @property {Boolean} enableDNSCache - Enable DNS lookup from local cache or not, default is false.
* @property {Boolean} dnsCacheLookupInterval - minimum interval of DNS query on the same hostname (default 10s).
* @property {Boolean} enableDNSCache - Enable DNS lookup cache using dns.lookup (old behavior), default is false.
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment has incorrect grammar. "Enable DNS lookup cache using dns.lookup (old behavior)" should clarify what "old behavior" refers to - it's better to say "Enable DNS lookup cache (using dns.lookup, the legacy mode)" or "Enable DNS lookup cache using dns.lookup mode".

Copilot uses AI. Check for mistakes.
* @property {Number} dnsCacheLookupInterval - DNS cache lookup interval in ms for old dns.lookup mode, default is 10000 ms.
*
* @property {Boolean} useDNSResolver - Enable DNS resolve cache using dns.resolve (new feature), default is false.
* @property {Array<String>} dnsServers - Custom DNS nameservers for DNS resolver cache, e.g. ['8.8.8.8', '1.1.1.1']. If not set, use system default.
*
* @property {Number} dnsCacheMaxLength - DNS cache max size, default is 1000.
* @property {Boolean} dnsAddressRotation - Enable address rotation for both lookup and resolve modes, default is true.
*
* @property {Number} request.timeout - httpclient request default timeout, default is 5000 ms.
*
Expand All @@ -307,12 +313,21 @@ module.exports = appInfo => {
* @property {Boolean} allowH2 - use urllib@4 HttpClient and enable H2, default is false. Only works on Node.js >= 18
*/
config.httpclient = {
// Enable DNS cache mode using dns.lookup
enableDNSCache: false,
dnsCacheLookupInterval: 10000,
dnsCacheLookupInterval: 10000, // will not work if useDNSResolver is true

// DNS resolver mode using dns.resolve (new feature)
useDNSResolver: false, // will not work if enableDNSCache IS NOT true
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message uses "IS NOT" in all caps which is unconventional. Standard practice is to use lowercase "is not" or simply rephrase as "will not work when enableDNSCache is false" for better readability.

Copilot uses AI. Check for mistakes.
dnsServers: undefined, // Use system default if not set

// Common dns options
dnsCacheMaxLength: 1000,
dnsAddressRotation: true,

request: {
timeout: 5000,
reset: false, // only works when useHttpClientNext is true
},
httpAgent: {
keepAlive: true,
Expand Down
13 changes: 13 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ declare module 'egg' {
new(ctx: Context): EggContextHttpClient;
}

export interface EggContextFetch {
ctx: Context;
app: Application;
Comment on lines +93 to +94
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这两个内部细节不应该对外透出了。

fetch(url: string | URL, init?: RequestInit): Promise<Response>;
/**
* safeFetch request helper with SSRF protection
*/
safeFetch(url: string | URL, init?: RequestInit): Promise<Response>;
}
interface EggContextFetchConstructor {
new (ctx: Context): EggContextFetch;
}

/**
* BaseContextClass is a base class that can be extended,
* it's instantiated in context level,
Expand Down
36 changes: 36 additions & 0 deletions lib/core/context_fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class ContextFetch {
constructor(ctx) {
this.ctx = ctx;
this.app = ctx.app;
}

/**
* fetch request helper based on native fetch API or urllib4's FetchFactory
* Keep the same api with {@link Application#fetch}.
*
* @param {String|URL} url - request url address.
* @param {Object} [init] - fetch init options.
* @return {Promise<Response>} fetch response
*/
async fetch(url, init) {
init = init || {};
init.ctx = this.ctx;
return await this.app.fetch(url, init);
}

/**
* safeFetch request helper with SSRF protection
* Keep the same api with {@link Application#safeFetch}.
*
* @param {String|URL} url - request url address.
* @param {Object} [init] - fetch init options.
* @return {Promise<Response>} fetch response
*/
async safeFetch(url, init) {
init = init || {};
init.ctx = this.ctx;
return await this.app.safeFetch(url, init);
}
}

module.exports = ContextFetch;
Loading
Loading