Skip to content

feat: pick dns cache commits to v4#5841

Merged
fengmk2 merged 4 commits intoeggjs:nextfrom
Dipper30:feat/pick-httpclient-dnscache-features
Mar 31, 2026
Merged

feat: pick dns cache commits to v4#5841
fengmk2 merged 4 commits intoeggjs:nextfrom
Dipper30:feat/pick-httpclient-dnscache-features

Conversation

@Dipper30
Copy link
Copy Markdown
Contributor

@Dipper30 Dipper30 commented Mar 27, 2026

PR Description
This PR ports the complete custom DNS lookup and httpclient proxy/interceptor module design from 3.x to the next branch.
Key changes:

Implements createTransparentProxy utility for lazy, mm()-compatible httpClient instantiation.
Refactors app.httpClient to use the proxy, enabling plugins to modify config before real client creation.
Adds support for config.httpclient.interceptors (undici dispatcher interceptors) and applies them in httpclient constructor.
Provides full unit tests for proxy and interceptor logic.
Ensures all changes are TypeScript, ESM, and monorepo compatible.
All tests pass except for known unrelated flaky cases.

Summary by CodeRabbit

  • New Features

    • Add middleware-style HTTP client interceptors via configuration to modify outgoing requests.
    • HTTP client now lazily initializes on first use (deferred construction).
    • Public utility for creating transparent/lazy proxies (with configurable binding) is now exposed from the package entrypoint.
  • Tests

    • New test suites and app fixtures validating interceptor behavior, proxy semantics, lazy initialization, and compatibility with mocking utilities.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d1f8c1e-0448-45e8-9127-eb10496969b9

📥 Commits

Reviewing files that changed from the base of the PR and between d29f968 and d37ed7c.

📒 Files selected for processing (1)
  • packages/egg/src/lib/egg.ts

📝 Walkthrough

Walkthrough

Added a lazy transparent proxy for HttpClient creation, composed configurable dispatcher interceptors at HttpClient construction, and extended types, tests, and fixtures to validate proxy semantics and interceptor behavior.

Changes

Cohort / File(s) Summary
Transparent Proxy Utility
packages/egg/src/lib/core/utils.ts, packages/egg/src/index.ts
Add createTransparentProxy<T>(options) and CreateTransparentProxyOptions with lazy real-object creation, overlay property semantics, optional function binding; re-exported from package entry.
HttpClient & Types
packages/egg/src/lib/core/httpclient.ts, packages/egg/src/lib/types.ts
HttpClient constructor now composes app.config.httpclient.interceptors via Dispatcher.compose(...) when configured and replaces the dispatcher. Add interceptors?: Dispatcher.DispatcherComposeInterceptor[] to HttpClientConfig.
EggApplicationCore integration
packages/egg/src/lib/egg.ts
EggApplicationCore.httpClient now stored as a createTransparentProxy<HttpClient> whose createReal invokes createHttpClient(), deferring real instantiation until first use.
Test fixtures & configs
packages/egg/test/fixtures/apps/httpclient-interceptor/*, packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js
New fixture app with interceptor injecting x-trace-id and incremental x-rpc-id; per-worker temp paths for logs/run added to fixtures.
Tests
packages/egg/test/lib/core/utils.test.ts, packages/egg/test/lib/core/httpclient_proxy.test.ts, packages/egg/test/lib/core/httpclient_interceptor.test.ts
Add suites covering transparent proxy semantics (lazy init, traps, binding, overlay), proxy interoperability/mocking/DNS, and interceptor header injection / monotonic rpc id behavior.

Sequence Diagram(s)

sequenceDiagram
    participant App as EggApplicationCore
    participant Proxy as HttpClient Proxy
    participant HC as HttpClient
    participant Disp as Dispatcher
    participant Intercept as Interceptor Chain
    participant Net as Network

    App->>Proxy: access httpClient (first time)
    Proxy->>Proxy: detect property/method access
    Proxy->>HC: call createReal() -> createHttpClient()
    HC->>Disp: read current dispatcher
    HC->>Intercept: Dispatcher.compose(config.interceptors)
    HC->>Disp: setDispatcher(composed)
    HC-->>Proxy: return proxied HttpClient

    App->>Proxy: httpClient.request(opts)
    Proxy->>HC: forward request(opts)
    HC->>Disp: dispatch(opts, handler)
    Disp->>Intercept: run interceptors (inject headers)
    Intercept->>Net: perform network request
    Net-->>Intercept: response
    Intercept-->>HC: response
    HC-->>App: response
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

core: httpclient, type: feature

Suggested reviewers

  • gxkl

Poem

🐰 I waited till first knock, then sprang alive,

A proxy stitched, so real clients need not strive.
Interceptors hop in, trace and rpc count too,
Bound methods kept snug, lazy-init through and through—🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'feat: pick dns cache commits to v4' is misleading; the PR actually ports httpclient proxy/interceptor features to the next branch, not v4, and covers more than just DNS cache. Revise the title to accurately reflect the scope, such as 'feat: port httpclient proxy and interceptor features to next' or 'feat: add createTransparentProxy utility and httpclient interceptor support'.
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a createTransparentProxy utility to enable lazy initialization of the HttpClient, allowing plugins to modify configurations during lifecycle hooks before the client is instantiated. It also adds support for request interceptors via Dispatcher.compose(). Feedback was provided regarding the use of a module-level variable for state in a test fixture, which could lead to race conditions in concurrent environments.

Comment on lines +3 to +13
let rpcIdCounter = 0;

exports.httpclient = {
interceptors: [
// Tracer interceptor: injects trace headers into every request
(dispatch) => {
return (opts, handler) => {
opts.headers = opts.headers || {};
opts.headers['x-trace-id'] = 'trace-123';
rpcIdCounter++;
opts.headers['x-rpc-id'] = `rpc-${rpcIdCounter}`;
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.

medium

Using a module-level variable rpcIdCounter for state can be problematic in a concurrent environment, as it creates shared mutable state. While this might be acceptable for this specific test which runs requests sequentially, it's a pattern that can lead to race conditions in a real application. To promote better practices even in tests, consider managing this state without relying on a shared module-level variable. For example, you could use a factory function that creates an interceptor with its own encapsulated counter.

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.29%. Comparing base (b7008e6) to head (d37ed7c).
⚠️ Report is 1 commits behind head on next.

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5841      +/-   ##
==========================================
+ Coverage   85.24%   85.29%   +0.05%     
==========================================
  Files         666      666              
  Lines       13171    13204      +33     
  Branches     1522     1529       +7     
==========================================
+ Hits        11227    11262      +35     
+ Misses       1813     1811       -2     
  Partials      131      131              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
packages/egg/test/lib/core/utils.test.ts (1)

7-7: Minor inconsistency: describe name references .js but file is .ts.

The describe block name references utils.test.js but the actual file is utils.test.ts.

♻️ Suggested fix
-describe('test/lib/core/utils.test.js', () => {
+describe('test/lib/core/utils.test.ts', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/egg/test/lib/core/utils.test.ts` at line 7, The describe block
string is inconsistent with the TypeScript file name; update the test suite
title used in the describe(...) invocation (currently
"test/lib/core/utils.test.js") to reference the correct file name
"test/lib/core/utils.test.ts" so the description matches the actual file and
test conventions.
packages/egg/test/lib/core/httpclient_interceptor.test.ts (2)

21-26: Consider promisifying server close for reliable cleanup.

http.Server.close() is callback-based and doesn't return a Promise. In rare cases, the server might not fully close before app.close() runs or before the next test suite starts.

♻️ Suggested improvement
     afterAll(async () => {
       if (serverInfo?.server?.listening) {
-        serverInfo.server.close();
+        await new Promise<void>(resolve => serverInfo.server.close(() => resolve()));
       }
       await app.close();
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/egg/test/lib/core/httpclient_interceptor.test.ts` around lines 21 -
26, The afterAll cleanup uses the callback-based serverInfo.server.close() which
may not finish before awaiting app.close(); wrap serverInfo.server.close() in a
Promise (or use util.promisify) and await it before calling await app.close() to
ensure the HTTP server is fully closed; update the afterAll block to await the
promised server close (referencing serverInfo.server.close and app.close) so
teardown is deterministic.

66-71: Same server close pattern - apply consistent fix.

Same callback-based close pattern as the first describe block.

♻️ Suggested improvement
     afterAll(async () => {
       if (serverInfo?.server?.listening) {
-        serverInfo.server.close();
+        await new Promise<void>(resolve => serverInfo.server.close(() => resolve()));
       }
       await app.close();
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/egg/test/lib/core/httpclient_interceptor.test.ts` around lines 66 -
71, In the afterAll block, the server is closed via the callback-based
serverInfo.server.close(); make this consistent with the first describe: when
serverInfo?.server?.listening, await closing the server by wrapping
serverInfo.server.close in a Promise (e.g., await new Promise(resolve =>
serverInfo.server.close(() => resolve()))), then await app.close(); update the
afterAll that contains serverInfo.server.close to use this same Promise-wrapped
await pattern so the server fully closes before app.close() runs.
packages/egg/test/lib/core/httpclient_proxy.test.ts (2)

21-28: Consider promisifying server close for reliable cleanup.

Same pattern as other test files - http.Server.close() is callback-based. Also, afterEach(mm.restore) is a good practice for ensuring mock cleanup.

♻️ Suggested improvement
   afterAll(async () => {
     if (serverInfo?.server?.listening) {
-      serverInfo.server.close();
+      await new Promise<void>(resolve => serverInfo.server.close(() => resolve()));
     }
     await app.close();
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/egg/test/lib/core/httpclient_proxy.test.ts` around lines 21 - 28,
The test teardown should await the server close to ensure reliable cleanup:
replace the callback-based call to serverInfo.server.close() in the afterAll
block with a promisified wait (e.g. await a Promise that resolves when
serverInfo.server.close's callback runs) so the process waits for the server to
actually close; keep the afterEach(mm.restore) mock restore as-is to ensure
mocks are cleaned up.

80-117: Redundant mm.restore() call inside test.

Line 95 calls mm.restore() manually, but afterEach(mm.restore) on line 28 already handles cleanup. The manual call is harmless but redundant.

♻️ Optional: Remove redundant restore

If the intent is to test behavior after restore within the same test, keep it. Otherwise, this can be removed since afterEach handles cleanup:

     assert.deepEqual(res.data, { mocked: true });
 
-    // Restore
-    mm.restore();
-
     // After restore, should use real HttpClient again

However, if you need to verify post-restore behavior in the same test (as the current code does), keep the manual call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/egg/test/lib/core/httpclient_proxy.test.ts` around lines 80 - 117,
The test "should support mm() mock on httpClient (egg-mock compatibility)"
contains a redundant manual mm.restore() call (mm.restore) because
afterEach(mm.restore) already cleans up mocks; remove the explicit mm.restore()
in that test unless you intentionally want to verify behavior after restore (in
which case keep it and add a comment clarifying that intent), referencing the
test name and the mm.restore() invocation to locate the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/egg/src/lib/types.ts`:
- Around line 71-88: The interceptors property is typed as
Dispatcher.DispatcherComposeInterceptor which doesn't exist; change its type to
the correct undici type (DispatcherComposeInterceptor) or import and use that
type directly. Update the declaration of interceptors?:
Dispatcher.DispatcherComposeInterceptor[] to use DispatcherComposeInterceptor[]
(or import { DispatcherComposeInterceptor } from 'undici' and use that), or
alternatively declare it as (dispatch: Dispatcher['dispatch']) =>
Dispatcher['dispatch'][] if you prefer inline typing; ensure references to
Dispatcher.DispatcherComposeInterceptor are removed and replaced with
DispatcherComposeInterceptor or the equivalent imported/inline type so
TypeScript compiles.

---

Nitpick comments:
In `@packages/egg/test/lib/core/httpclient_interceptor.test.ts`:
- Around line 21-26: The afterAll cleanup uses the callback-based
serverInfo.server.close() which may not finish before awaiting app.close(); wrap
serverInfo.server.close() in a Promise (or use util.promisify) and await it
before calling await app.close() to ensure the HTTP server is fully closed;
update the afterAll block to await the promised server close (referencing
serverInfo.server.close and app.close) so teardown is deterministic.
- Around line 66-71: In the afterAll block, the server is closed via the
callback-based serverInfo.server.close(); make this consistent with the first
describe: when serverInfo?.server?.listening, await closing the server by
wrapping serverInfo.server.close in a Promise (e.g., await new Promise(resolve
=> serverInfo.server.close(() => resolve()))), then await app.close(); update
the afterAll that contains serverInfo.server.close to use this same
Promise-wrapped await pattern so the server fully closes before app.close()
runs.

In `@packages/egg/test/lib/core/httpclient_proxy.test.ts`:
- Around line 21-28: The test teardown should await the server close to ensure
reliable cleanup: replace the callback-based call to serverInfo.server.close()
in the afterAll block with a promisified wait (e.g. await a Promise that
resolves when serverInfo.server.close's callback runs) so the process waits for
the server to actually close; keep the afterEach(mm.restore) mock restore as-is
to ensure mocks are cleaned up.
- Around line 80-117: The test "should support mm() mock on httpClient (egg-mock
compatibility)" contains a redundant manual mm.restore() call (mm.restore)
because afterEach(mm.restore) already cleans up mocks; remove the explicit
mm.restore() in that test unless you intentionally want to verify behavior after
restore (in which case keep it and add a comment clarifying that intent),
referencing the test name and the mm.restore() invocation to locate the change.

In `@packages/egg/test/lib/core/utils.test.ts`:
- Line 7: The describe block string is inconsistent with the TypeScript file
name; update the test suite title used in the describe(...) invocation
(currently "test/lib/core/utils.test.js") to reference the correct file name
"test/lib/core/utils.test.ts" so the description matches the actual file and
test conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e3c59b7c-7705-4ba1-b701-24f2f34b86e1

📥 Commits

Reviewing files that changed from the base of the PR and between 53bd879 and 843cfb0.

⛔ Files ignored due to path filters (2)
  • packages/egg/test/__snapshots__/index.test.ts.snap is excluded by !**/*.snap
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (11)
  • packages/egg/src/index.ts
  • packages/egg/src/lib/core/httpclient.ts
  • packages/egg/src/lib/core/utils.ts
  • packages/egg/src/lib/egg.ts
  • packages/egg/src/lib/types.ts
  • packages/egg/test/fixtures/apps/httpclient-interceptor/app/router.js
  • packages/egg/test/fixtures/apps/httpclient-interceptor/config/config.default.js
  • packages/egg/test/fixtures/apps/httpclient-interceptor/package.json
  • packages/egg/test/lib/core/httpclient_interceptor.test.ts
  • packages/egg/test/lib/core/httpclient_proxy.test.ts
  • packages/egg/test/lib/core/utils.test.ts

Comment on lines +71 to +88
/**
* Interceptors for request composition, applied via `Dispatcher.compose()`.
* Each interceptor receives a `dispatch` function and returns a new `dispatch` function.
*
* @example
* ```ts
* // config.default.ts
* config.httpclient = {
* interceptors: [
* (dispatch) => (opts, handler) => {
* opts.headers = { ...opts.headers, 'x-trace-id': generateTraceId() };
* return dispatch(opts, handler);
* },
* ],
* };
* ```
*/
interceptors?: Dispatcher.DispatcherComposeInterceptor[];
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.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

urllib npm package Dispatcher.DispatcherComposeInterceptor type

💡 Result:

No such type "Dispatcher.DispatcherComposeInterceptor" exists in the urllib npm package or its underlying undici dependency. Urllib (v4.9.0, latest as of 2025-12-13) is a high-level HTTP client built on undici that exports undici's Dispatcher types like Agent, ProxyAgent, MockAgent, but does not define custom types like DispatcherComposeInterceptor. Undici (v7.1.1 dependency) has Dispatcher.compose method which accepts interceptors of type DispatcherComposeInterceptor (in newer versions) or DispatchInterceptor (older deprecated versions). This type is: type DispatcherComposeInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch']; It takes the base dispatch function and returns a new dispatch-like function for chaining interceptors (e.g., retry, redirect). Example usage from undici docs: const { Agent, interceptors } = require('undici'); const agent = new Agent.compose( interceptors.retry, interceptors.redirect ); Urllib usage example with custom dispatcher: import { ProxyAgent, request } from 'urllib'; const proxyAgent = new ProxyAgent('http://proxy:8080'); const response = await request('https://example.com', { dispatcher: proxyAgent }); To use compose interceptors in urllib, pass a composed undici Dispatcher via options.dispatcher.

Citations:


🏁 Script executed:

cat -n packages/egg/src/lib/types.ts | head -100

Repository: eggjs/egg

Length of output: 4100


🏁 Script executed:

rg "Dispatcher" packages/egg/src/lib/types.ts -B 5 -A 5

Repository: eggjs/egg

Length of output: 1194


🏁 Script executed:

rg "import.*Dispatcher" packages/egg/src/lib/ -t ts

Repository: eggjs/egg

Length of output: 124


🏁 Script executed:

cat packages/egg/package.json | grep -A 5 -B 5 "urllib\|undici"

Repository: eggjs/egg

Length of output: 1058


🏁 Script executed:

rg "DispatcherComposeInterceptor" packages/ -t ts

Repository: eggjs/egg

Length of output: 142


🏁 Script executed:

find packages -name "*.d.ts" -type f | xargs rg "DispatcherComposeInterceptor" 2>/dev/null || echo "No .d.ts files found with this type"

Repository: eggjs/egg

Length of output: 88


🏁 Script executed:

# Check urllib package.json to see what version is used
rg "urllib" packages/egg/package.json -A 2 -B 2

Repository: eggjs/egg

Length of output: 514


🏁 Script executed:

# Check the root pnpm-workspace.yaml or package.json for version catalog
cat pnpm-workspace.yaml 2>/dev/null || cat package.json | grep -A 50 "catalog"

Repository: eggjs/egg

Length of output: 5462


🏁 Script executed:

# Look for urllib version in root catalog
rg "urllib.*:" . --type yaml --type json | head -20

Repository: eggjs/egg

Length of output: 859


🏁 Script executed:

# Check if there's a monorepo-level package.json that defines catalog versions
cat package.json | jq '.catalog' 2>/dev/null || grep -A 100 '"catalog"' package.json | head -50

Repository: eggjs/egg

Length of output: 57


Type reference Dispatcher.DispatcherComposeInterceptor does not exist in urllib.

The type Dispatcher.DispatcherComposeInterceptor is not exported from urllib v4.8.2 or its underlying undici dependency. The actual type from undici is DispatcherComposeInterceptor (without the Dispatcher namespace), defined as (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch']. This code will fail TypeScript compilation. The type annotation should be corrected to reference the proper type from undici, or import DispatcherComposeInterceptor directly rather than accessing it as a namespace member.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/egg/src/lib/types.ts` around lines 71 - 88, The interceptors
property is typed as Dispatcher.DispatcherComposeInterceptor which doesn't
exist; change its type to the correct undici type (DispatcherComposeInterceptor)
or import and use that type directly. Update the declaration of interceptors?:
Dispatcher.DispatcherComposeInterceptor[] to use DispatcherComposeInterceptor[]
(or import { DispatcherComposeInterceptor } from 'undici' and use that), or
alternatively declare it as (dispatch: Dispatcher['dispatch']) =>
Dispatcher['dispatch'][] if you prefer inline typing; ensure references to
Dispatcher.DispatcherComposeInterceptor are removed and replaced with
DispatcherComposeInterceptor or the equivalent imported/inline type so
TypeScript compiles.

@socket-security
Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedlodash@​4.17.2175998784100
Addedcross-spawn@​7.0.6991009780100

View full report

@fengmk2 fengmk2 merged commit 5c459e4 into eggjs:next Mar 31, 2026
17 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants