Skip to content

Conversation

@Dipper30
Copy link
Contributor

@Dipper30 Dipper30 commented Dec 25, 2025

pick from #5749

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.

✏️ Tip: You can customize this high-level summary in your review settings.

Copilot AI review requested due to automatic review settings December 25, 2025 04:23
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 25, 2025

📝 Walkthrough

Walkthrough

This PR adds support for custom DNS lookup functions in HttpClient configuration. It introduces a LookupFunction type to HttpClientConfig, updates the HttpClient constructor to safely handle missing config and inherit lookup settings, replaces legacy DNS caching configuration with a custom lookup implementation in test fixtures, and adds comprehensive tests with a new local server utility.

Changes

Cohort / File(s) Summary
Core HttpClient Support
packages/egg/src/lib/core/httpclient.ts, packages/egg/src/lib/types.ts
Added LookupFunction type import from node:net; extended HttpClientConfig interface with optional lookup property; modified constructor to fallback to empty object when httpclient config is undefined and use nullish coalescing for lookup inheritance.
Test Configuration & Fixture
packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js
Replaced enableDNSCache and dnsCacheLookupInterval with custom httpclient.lookup function; implements callback-based lookup that returns IP literals directly and resolves non-IP hostnames to 127.0.0.1, supporting both single and array response modes.
Test Suite & Utilities
packages/egg/test/lib/core/dnscache_httpclient.test.ts, packages/egg/test/utils.ts
Replaced skipped tests with active Vitest suite; added beforeAll/afterAll lifecycle hooks using local server utility; added startNewLocalServer utility function that creates ephemeral Koa HTTP server with /get_headers and basic response endpoints and includes retry logic for port conflicts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • eggjs/egg#5383: Modifies HttpClient implementation and httpclient config types in matching files, providing related context for this custom lookup feature.

Suggested labels

core: httpclient, type: feature

Poem

🐰 A lookup function hops into place,
DNS caching joins the past's embrace,
Custom handlers now take the reins,
Tests spring alive (no more strains!),
Configuration dances—fluffy and bright! 🌟

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: enable custom lookup' directly and concisely describes the main feature added across all modified files—enabling a custom lookup function configuration in HttpClientConfig.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @Dipper30, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant enhancement to the HTTP client by enabling custom DNS lookup functionality. This allows for greater control over how hostnames are resolved, which is particularly beneficial for advanced network configurations, testing environments, and local development. The changes involve modifying the HTTP client's configuration handling, updating relevant type definitions, and a comprehensive refactoring of the associated test suite to validate the new feature's behavior.

Highlights

  • Custom DNS Lookup: The HTTP client now supports a custom DNS lookup function, allowing users to define their own hostname resolution logic through options.lookup or app.config.httpclient.lookup.
  • Type Definition Update: The HttpClientConfig interface has been updated to include an optional lookup property, typed as LookupFunction from Node.js's net module.
  • Test Configuration Change: The test fixture dnscache_httpclient now uses a custom lookup function in its default configuration, replacing the previous enableDNSCache and dnsCacheLookupInterval settings. This custom lookup resolves hostnames to 127.0.0.1 for testing purposes.
  • Refactored Test Suite: The dnscache_httpclient.test.ts file has been completely rewritten and re-enabled, introducing new tests for app.curl and app.safeCurl with the custom lookup, and utilizing a new startNewLocalServer utility.
  • New Test Utility: A new utility function, startNewLocalServer, has been added to test/utils.ts to facilitate creating and managing local Koa HTTP servers for testing scenarios.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@codecov
Copy link

codecov bot commented Dec 25, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.60%. Comparing base (0cf6a3e) to head (1661624).
⚠️ Report is 27 commits behind head on next.

Additional details and impacted files
@@            Coverage Diff             @@
##             next    #5751      +/-   ##
==========================================
+ Coverage   87.40%   87.60%   +0.20%     
==========================================
  Files         561      563       +2     
  Lines       10932    10931       -1     
  Branches     1238     1241       +3     
==========================================
+ Hits         9555     9576      +21     
+ Misses       1293     1271      -22     
  Partials       84       84              

☔ 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
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 enables the use of a custom lookup function in httpclient, which is a great move towards using standard Node.js features and simplifying the codebase by removing the previous custom DNS caching logic. The implementation is straightforward and the related type definitions and test cases have been updated accordingly.

I've found a couple of minor issues in the test files: a duplicated test case and a potential race condition in a new test utility. Please see my comments for details.

Comment on lines +44 to +48
it('should safeCurl also work', async () => {
// will resolve custom-localhost to 127.0.0.1
const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
assert(res.status === 200);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

This test case is a duplicate of the one from lines 38-42. It should be removed to avoid redundancy and keep the test suite clean.

Comment on lines +157 to +193
export async function startNewLocalServer(ip = '127.0.0.1'): Promise<{
url: string;
server: http.Server;
}> {
let localServer: http.Server;
return new Promise((resolve, reject) => {
const app = new Koa();
app.use(async (ctx) => {
if (ctx.path === '/get_headers') {
ctx.body = {
headers: ctx.request.headers,
host: ctx.request.headers.host,
};
return;
}
ctx.body = JSON.stringify(`${ctx.method} ${ctx.path}`);
});
localServer = http.createServer(app.callback());
const serverCallback = () => {
const addressRes = localServer.address();
const port = addressRes && typeof addressRes === 'object' ? addressRes.port : addressRes;
const url = `http://${ip}:` + port;
return resolve({ url, server: localServer });
};
localServer.listen(0, serverCallback);
localServer.on('error', (e: any) => {
if (e.code === 'EADDRINUSE') {
setTimeout(() => {
localServer.close();
localServer.listen(0, serverCallback);
}, 1000);
} else {
reject(e);
}
});
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The EADDRINUSE error handling in startNewLocalServer contains a race condition. localServer.close() is asynchronous, but it's not awaited before localServer.listen() is called again inside a setTimeout.

Additionally, since listen(0) is used to pick a random available port, an EADDRINUSE error is highly unlikely.

I suggest refactoring this function to use async/await and simplifying the error handling, similar to the existing startLocalServer function in this file. This makes the code cleaner and avoids the potential race condition.

export async function startNewLocalServer(ip = '127.0.0.1'): Promise<{
  url: string;
  server: http.Server;
}> {
  const app = new Koa();
  app.use(async ctx => {
    if (ctx.path === '/get_headers') {
      ctx.body = {
        headers: ctx.request.headers,
        host: ctx.request.headers.host,
      };
      return;
    }
    ctx.body = JSON.stringify(`${ctx.method} ${ctx.path}`);
  });
  const localServer = http.createServer(app.callback());
  localServer.listen(0);
  await once(localServer, 'listening');
  const address = localServer.address() as AddressInfo;
  const url = `http://${ip}:${address.port}`;
  return { url, server: localServer };
}

Copy link
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: 5

🧹 Nitpick comments (2)
packages/egg/src/lib/types.ts (1)

69-69: Consider adding JSDoc documentation for the new lookup property.

Other properties in HttpClientConfig have comments. For consistency and discoverability, consider documenting this option.

🔎 Suggested documentation
   allowH2?: boolean;
+  /**
+   * Custom DNS lookup function for resolving hostnames
+   */
   lookup?: LookupFunction;
packages/egg/test/utils.ts (1)

157-173: Consider consolidating with existing startLocalServer to reduce duplication.

There's already a startLocalServer function (lines 80-125) with similar functionality. The main differences are:

  • startNewLocalServer returns the server instance for caller-managed cleanup
  • /get_headers returns different response format

Consider whether these could be unified or if the differences warrant a clearer naming distinction.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9eeb060 and 1661624.

📒 Files selected for processing (5)
  • packages/egg/src/lib/core/httpclient.ts
  • packages/egg/src/lib/types.ts
  • packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js
  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
  • packages/egg/test/utils.ts
🧰 Additional context used
📓 Path-based instructions (10)
packages/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/**/*.ts: Use TypeScript throughout all packages in the Eggjs monorepo
Use strict TypeScript mode in all packages

Files:

  • packages/egg/src/lib/core/httpclient.ts
  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
  • packages/egg/src/lib/types.ts
  • packages/egg/test/utils.ts
packages/egg/src/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Work primarily in 'packages/egg/src/' for core framework features, organizing into lib/, app/extend/, config/, and lib/loader/ directories

Files:

  • packages/egg/src/lib/core/httpclient.ts
  • packages/egg/src/lib/types.ts
**/*.{ts,tsx,js,mjs}

📄 CodeRabbit inference engine (AGENTS.md)

Use .ts sources over CommonJS and prefer ESM for exports

Use ESM (ES Modules) syntax with 'import' and 'export' statements - add 'type: module' to package.json for CommonJS to ESM migration

Files:

  • packages/egg/src/lib/core/httpclient.ts
  • packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js
  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
  • packages/egg/src/lib/types.ts
  • packages/egg/test/utils.ts
{packages,plugins}/**/*.{ts,tsx,js,mjs}

📄 CodeRabbit inference engine (AGENTS.md)

Name files in lowercase with hyphens (e.g. loader-context.ts)

Files:

  • packages/egg/src/lib/core/httpclient.ts
  • packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js
  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
  • packages/egg/src/lib/types.ts
  • packages/egg/test/utils.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Name classes in PascalCase
Name functions and variables in camelCase

Files:

  • packages/egg/src/lib/core/httpclient.ts
  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
  • packages/egg/src/lib/types.ts
  • packages/egg/test/utils.ts
**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.ts: All exported functions and methods must have explicit return type annotations to support TypeScript's isolatedDeclarations flag
Avoid using computed property names with symbols in class declarations - use override methods instead for custom property definitions
Use explicit type annotations for class properties and add 'unique symbol' type for exported symbols to support isolatedDeclarations
Enable strict mode in all TypeScript packages and use explicit return types for public APIs
Prefer interfaces over type aliases for object shapes in TypeScript
Use readonly modifiers where appropriate in TypeScript class properties and interface definitions
Avoid using 'any' type in TypeScript - use 'unknown' when type is truly unknown
Use BaseContextClass as the base class for context-aware components like services and subscriptions in Egg applications
Monitor memory usage and implement proper cleanup in lifecycle hooks to avoid memory leaks in long-running processes
Avoid global state in worker processes - use application context or request context for state management

Files:

  • packages/egg/src/lib/core/httpclient.ts
  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
  • packages/egg/src/lib/types.ts
  • packages/egg/test/utils.ts
packages/egg/test/fixtures/apps/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Create test fixtures in 'packages/egg/test/fixtures/apps/' for scenario testing

Files:

  • packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js
**/test/fixtures/**

📄 CodeRabbit inference engine (AGENTS.md)

Place reusable test data under test/fixtures/

Files:

  • packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js
packages/**/test/**/*.test.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

packages/**/test/**/*.test.ts: Use Vitest as the standard test runner for all packages; test files follow pattern 'test/**/*.test.ts'
Use 'import { describe, it } from "vitest"' for test functions in Vitest test files
Use Node.js built-in 'assert' module for assertions in tests

Files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
**/test/**/*.test.ts

📄 CodeRabbit inference engine (AGENTS.md)

Configure Vitest to discover **/test/**/*.test.ts within each package and mirror this pattern when adding test suites

**/test/**/*.test.ts: Test files in Vitest-based packages must follow the naming pattern 'test/**/*.test.ts' and import test functions from vitest
Configure test files to use Node.js built-in 'assert' module for assertions in Vitest tests
Import test functions (describe, it, expect, beforeEach, afterEach) from 'vitest' package in test files

Files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
🧠 Learnings (13)
📚 Learning: 2025-12-21T14:11:29.307Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T14:11:29.307Z
Learning: Applies to **/test/**/*.test.ts : Import test functions (describe, it, expect, beforeEach, afterEach) from 'vitest' package in test files

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-11-27T14:37:59.711Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T14:37:59.711Z
Learning: Applies to packages/**/test/**/*.test.ts : Use 'import { describe, it } from "vitest"' for test functions in Vitest test files

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-12-21T14:11:29.307Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T14:11:29.307Z
Learning: Applies to **/test/**/*.test.ts : Configure test files to use Node.js built-in 'assert' module for assertions in Vitest tests

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-11-27T14:37:59.711Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T14:37:59.711Z
Learning: Applies to packages/**/test/**/*.test.ts : Use Node.js built-in 'assert' module for assertions in tests

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-12-20T09:24:59.960Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-20T09:24:59.960Z
Learning: Add regression cases that exercise both CommonJS and TypeScript example apps when features affect HTTP or process orchestration

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-12-21T14:11:29.307Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T14:11:29.307Z
Learning: Applies to **/test/**/*.test.ts : Test files in Vitest-based packages must follow the naming pattern 'test/**/*.test.ts' and import test functions from vitest

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-12-21T14:11:29.307Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T14:11:29.307Z
Learning: Applies to **vitest.config.ts : All plugin Vitest configurations should use a vitest.config.ts file for test setup and configuration

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-12-21T14:11:29.307Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T14:11:29.307Z
Learning: All new packages MUST use Vitest for testing instead of other test runners. Exception: egg-bin and cookies packages use Mocha for consistency with their testing patterns.

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-12-21T03:05:13.313Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: tegg/CLAUDE.md:0-0
Timestamp: 2025-12-21T03:05:13.313Z
Learning: Applies to tegg/**/__tests__/**/*.test.{ts,tsx,js} : Use Vitest for testing all tegg packages

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-11-27T14:37:59.711Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-11-27T14:37:59.711Z
Learning: Applies to packages/**/test/**/*.test.ts : Use Vitest as the standard test runner for all packages; test files follow pattern 'test/**/*.test.ts'

Applied to files:

  • packages/egg/test/lib/core/dnscache_httpclient.test.ts
📚 Learning: 2025-12-21T14:11:29.307Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T14:11:29.307Z
Learning: Applies to plugins/*/src/types.ts : All module augmentation in plugin types.ts files must use 'declare module "egg"' syntax to extend Egg interfaces

Applied to files:

  • packages/egg/src/lib/types.ts
📚 Learning: 2025-12-21T14:11:29.307Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T14:11:29.307Z
Learning: Applies to plugins/*/src/types.ts : All Egg framework plugins MUST define a 'src/types.ts' file that extends the Egg module declarations using 'declare module "egg"' and defines typed configuration

Applied to files:

  • packages/egg/src/lib/types.ts
📚 Learning: 2025-12-21T14:11:29.307Z
Learnt from: CR
Repo: eggjs/egg PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-21T14:11:29.307Z
Learning: Applies to packages/egg/src/lib/start.ts : Implement graceful shutdown in application startup to support zero-downtime deployments

Applied to files:

  • packages/egg/test/utils.ts
🧬 Code graph analysis (3)
packages/egg/src/lib/core/httpclient.ts (6)
packages/core/src/egg.ts (1)
  • config (315-317)
packages/core/src/loader/egg_loader.ts (2)
  • config (1027-1031)
  • app (157-159)
packages/egg/src/lib/egg.ts (1)
  • config (562-564)
packages/egg/src/lib/core/base_hook_class.ts (2)
  • config (20-22)
  • app (24-27)
packages/egg/test/utils.ts (1)
  • app (31-36)
packages/core/src/lifecycle.ts (1)
  • app (128-130)
packages/egg/test/lib/core/dnscache_httpclient.test.ts (1)
packages/egg/test/utils.ts (4)
  • app (31-36)
  • MockApplication (24-24)
  • createApp (38-38)
  • startNewLocalServer (157-193)
packages/egg/test/utils.ts (4)
packages/egg/src/app/extend/context.ts (2)
  • ip (283-285)
  • ip (287-289)
packages/egg/src/app/extend/request.ts (2)
  • ip (139-148)
  • ip (161-163)
plugins/view/src/lib/view_manager.ts (1)
  • resolve (101-115)
packages/core/src/loader/egg_loader.ts (1)
  • app (157-159)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (20)
  • GitHub Check: CodeQL analysis (javascript-typescript)
  • GitHub Check: Agent
  • GitHub Check: Test (macos-latest, 20, 3/3)
  • GitHub Check: Test (ubuntu-latest, 24, 1/3)
  • GitHub Check: Test (ubuntu-latest, 20, 1/3)
  • GitHub Check: Test (ubuntu-latest, 24, 3/3)
  • GitHub Check: Test (windows-latest, 20, 2/3)
  • GitHub Check: Test (windows-latest, 24, 3/3)
  • GitHub Check: Test (macos-latest, 22, 3/3)
  • GitHub Check: Test (ubuntu-latest, 20, 3/3)
  • GitHub Check: Test (ubuntu-latest, 22, 1/3)
  • GitHub Check: Test (macos-latest, 22, 2/3)
  • GitHub Check: Test (ubuntu-latest, 22, 2/3)
  • GitHub Check: Test bin (ubuntu-latest, 24, 0/3)
  • GitHub Check: Test bin (windows-latest, 24, 2/3)
  • GitHub Check: Test bin (windows-latest, 24, 0/3)
  • GitHub Check: Test bin (ubuntu-latest, 24, 2/3)
  • GitHub Check: Test bin (ubuntu-latest, 24, 1/3)
  • GitHub Check: Test bin (windows-latest, 24, 1/3)
  • GitHub Check: typecheck
🔇 Additional comments (3)
packages/egg/src/lib/types.ts (1)

1-1: LGTM!

The import of LookupFunction from node:net is the correct standard Node.js type for custom DNS resolution callbacks.

packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js (1)

4-21: LGTM - appropriate custom lookup implementation for testing.

The lookup function correctly implements the Node.js dns.lookup callback signature, handling both single and all result modes. Hardcoding resolution to 127.0.0.1 for non-IP hostnames is suitable for this test fixture.

packages/egg/test/lib/core/dnscache_httpclient.test.ts (1)

1-6: LGTM on imports and test structure.

The test correctly uses Vitest imports (describe, it, beforeAll, afterAll) and Node.js built-in assert module as per the coding guidelines.

Comment on lines 26 to +29
constructor(app: EggApplicationCore, options: HttpClientOptions = {}) {
normalizeConfig(app);
const config = app.config.httpclient;
const config = app.config.httpclient || {};
options.lookup = options.lookup ?? config.lookup;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential runtime error in normalizeConfig when httpclient config is undefined.

The fallback to {} on line 28 is good, but normalizeConfig(app) is called before this assignment on line 27. Inside normalizeConfig (lines 62-66), app.config.httpclient is accessed directly without a fallback, which will cause a TypeError when accessing config.request?.timeout if config is undefined.

🔎 Proposed fix
 function normalizeConfig(app: EggApplicationCore) {
-  const config = app.config.httpclient;
+  const config = app.config.httpclient ?? {};
   if (typeof config.request?.timeout === 'string') {
     config.request.timeout = ms(config.request.timeout);
   }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(app: EggApplicationCore, options: HttpClientOptions = {}) {
normalizeConfig(app);
const config = app.config.httpclient;
const config = app.config.httpclient || {};
options.lookup = options.lookup ?? config.lookup;
function normalizeConfig(app: EggApplicationCore) {
const config = app.config.httpclient ?? {};
if (typeof config.request?.timeout === 'string') {
config.request.timeout = ms(config.request.timeout);
}
}
🤖 Prompt for AI Agents
In packages/egg/src/lib/core/httpclient.ts around lines 26-29,
normalizeConfig(app) is called before ensuring app.config.httpclient is defined,
causing a TypeError when normalizeConfig reads config.request?.timeout; either
move the const config = app.config.httpclient || {} and options.lookup
assignment above the normalizeConfig(app) call so normalizeConfig sees a defined
config, or modify normalizeConfig to accept a config parameter or use safe
guards (optional chaining / default {}) when reading app.config.httpclient;
implement one of these fixes and remove the unsafe direct access to
app.config.httpclient inside normalizeConfig.

Comment on lines +20 to +24
afterAll(() => {
if (serverInfo?.server?.listening) {
serverInfo.server.close();
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing app.close() in afterAll cleanup.

The application instance should be properly closed to release resources and avoid potential test interference. Based on coding guidelines, proper cleanup in lifecycle hooks is important to avoid memory leaks.

🔎 Proposed fix
   afterAll(() => {
     if (serverInfo?.server?.listening) {
       serverInfo.server.close();
     }
+    return app.close();
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
afterAll(() => {
if (serverInfo?.server?.listening) {
serverInfo.server.close();
}
});
afterAll(() => {
if (serverInfo?.server?.listening) {
serverInfo.server.close();
}
return app.close();
});
🤖 Prompt for AI Agents
In packages/egg/test/lib/core/dnscache_httpclient.test.ts around lines 20 to 24,
the afterAll cleanup closes the server but does not close the application
instance; update the afterAll hook to also close the app by checking for the app
variable (e.g., if (app && app.close)) and calling/awaiting app.close() so the
application is properly shut down and resources released after tests.

Comment on lines +44 to +48
it('should safeCurl also work', async () => {
// will resolve custom-localhost to 127.0.0.1
const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
assert(res.status === 200);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate test case - remove or rename.

This test (lines 44-48) is an exact duplicate of the test at lines 38-42. Both are named "should safeCurl also work" with identical implementations.

🔎 Proposed fix - remove duplicate
   it('should safeCurl also work', async () => {
     // will resolve custom-localhost to 127.0.0.1
     const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
     assert(res.status === 200);
   });
-
-  it('should safeCurl also work', async () => {
-    // will resolve custom-localhost to 127.0.0.1
-    const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
-    assert(res.status === 200);
-  });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should safeCurl also work', async () => {
// will resolve custom-localhost to 127.0.0.1
const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
assert(res.status === 200);
});
it('should safeCurl also work', async () => {
// will resolve custom-localhost to 127.0.0.1
const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
assert(res.status === 200);
});
🤖 Prompt for AI Agents
In packages/egg/test/lib/core/dnscache_httpclient.test.ts around lines 44-48,
there is an exact duplicate test of the one at lines 38-42 (both named "should
safeCurl also work"); remove the duplicate test or rename it if a distinct
behavior is intended — delete the redundant it(...) block at 44-48 so only one
test with that name and implementation remains, or change the duplicate's
description and implementation to cover the different case you actually want to
test.

Comment on lines +50 to +60
it('should server down and catch error', async () => {
try {
if (serverInfo?.server?.listening) await serverInfo.server.close();
// will resolve to 127.0.0.1
const res = await app.curl(url + '/get_headers', { dataType: 'json' });
assert(res.status !== 200);
} catch (err: any) {
assert(err);
assert(err.message.includes('ECONNREFUSED'));
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test logic issues in "server down" test.

  1. server.close() uses callback-style API; await doesn't wait for closure to complete
  2. The try-catch logic is unclear: if the request succeeds, it asserts status !== 200, which would fail for an actual successful response
🔎 Proposed fix
   it('should server down and catch error', async () => {
+    const { promisify } = await import('node:util');
     try {
-      if (serverInfo?.server?.listening) await serverInfo.server.close();
+      if (serverInfo?.server?.listening) {
+        await promisify(serverInfo.server.close.bind(serverInfo.server))();
+      }
       // will resolve to 127.0.0.1
       const res = await app.curl(url + '/get_headers', { dataType: 'json' });
-      assert(res.status !== 200);
+      assert.fail('Expected request to fail after server shutdown');
     } catch (err: any) {
       assert(err);
       assert(err.message.includes('ECONNREFUSED'));
     }
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('should server down and catch error', async () => {
try {
if (serverInfo?.server?.listening) await serverInfo.server.close();
// will resolve to 127.0.0.1
const res = await app.curl(url + '/get_headers', { dataType: 'json' });
assert(res.status !== 200);
} catch (err: any) {
assert(err);
assert(err.message.includes('ECONNREFUSED'));
}
});
it('should server down and catch error', async () => {
const { promisify } = await import('node:util');
try {
if (serverInfo?.server?.listening) {
await promisify(serverInfo.server.close.bind(serverInfo.server))();
}
// will resolve to 127.0.0.1
const res = await app.curl(url + '/get_headers', { dataType: 'json' });
assert.fail('Expected request to fail after server shutdown');
} catch (err: any) {
assert(err);
assert(err.message.includes('ECONNREFUSED'));
}
});
🤖 Prompt for AI Agents
In packages/egg/test/lib/core/dnscache_httpclient.test.ts around lines 50-60,
the "server down" test incorrectly awaits a callback-style server.close() and
allows a successful HTTP response path that asserts status !== 200; instead,
first properly wait for server shutdown by wrapping serverInfo.server.close in a
Promise (or use server.closeSync if available) so closure completes before
making the request, then call app.curl expecting it to throw; remove the flaky
assert(res.status !== 200) and instead assert failure if the request does not
throw (e.g., throw a test error after the call), and in the catch assert the
error exists and its message includes 'ECONNREFUSED'.

Comment on lines +182 to +191
localServer.on('error', (e: any) => {
if (e.code === 'EADDRINUSE') {
setTimeout(() => {
localServer.close();
localServer.listen(0, serverCallback);
}, 1000);
} else {
reject(e);
}
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

EADDRINUSE retry logic is flawed - closed server cannot be restarted.

After localServer.close() is called, the same server instance cannot be used to listen() again. A new server instance needs to be created for a retry attempt. Additionally, with port 0 (ephemeral port), EADDRINUSE is extremely unlikely since the OS assigns an available port.

🔎 Proposed fix - simplify by removing unlikely retry logic
     localServer.listen(0, serverCallback);
-    localServer.on('error', (e: any) => {
-      if (e.code === 'EADDRINUSE') {
-        setTimeout(() => {
-          localServer.close();
-          localServer.listen(0, serverCallback);
-        }, 1000);
-      } else {
-        reject(e);
-      }
-    });
+    localServer.on('error', reject);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
localServer.on('error', (e: any) => {
if (e.code === 'EADDRINUSE') {
setTimeout(() => {
localServer.close();
localServer.listen(0, serverCallback);
}, 1000);
} else {
reject(e);
}
});
localServer.listen(0, serverCallback);
localServer.on('error', reject);
🤖 Prompt for AI Agents
In packages/egg/test/utils.ts around lines 182 to 191, the EADDRINUSE retry
logic is incorrect because closing the existing server and attempting to reuse
it with listen() is invalid and, for port 0, unnecessary; remove the
special-case retry branch and treat any server 'error' by rejecting the promise
(or, if desired, create a brand-new server instance before retrying), so delete
the setTimeout/close/listen block and simply call reject(e) for all errors.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds support for custom DNS lookup functions in the Eggjs HTTP client, allowing applications to provide their own hostname resolution logic instead of relying solely on the default DNS lookup mechanism.

  • Adds a lookup property to the HttpClientConfig type definition
  • Integrates the custom lookup function into the HttpClient constructor
  • Replaces skipped DNS cache tests with new tests that verify custom lookup functionality

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/egg/src/lib/types.ts Imports LookupFunction from Node.js net module and adds it as an optional property to HttpClientConfig
packages/egg/src/lib/core/httpclient.ts Updates HttpClient constructor to merge custom lookup from config into options before passing to parent class
packages/egg/test/utils.ts Adds startNewLocalServer utility function to create a local test HTTP server with custom endpoints
packages/egg/test/lib/core/dnscache_httpclient.test.ts Replaces previously skipped tests with new tests that verify custom DNS lookup functionality using a mock resolver
packages/egg/test/fixtures/apps/dnscache_httpclient/config/config.default.js Configures a custom lookup function that resolves all non-IP hostnames to 127.0.0.1 for testing purposes

Comment on lines +44 to +49
it('should safeCurl also work', async () => {
// will resolve custom-localhost to 127.0.0.1
const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
assert(res.status === 200);
});

Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

Duplicate test case detected. Both test cases on lines 38-42 and 44-48 have identical descriptions "should safeCurl also work" and identical test logic. This creates confusion and is redundant. Consider removing one of these duplicate tests or differentiating them if they're intended to test different scenarios.

Suggested change
it('should safeCurl also work', async () => {
// will resolve custom-localhost to 127.0.0.1
const res = await app.safeCurl(url + '/get_headers', { dataType: 'json' });
assert(res.status === 200);
});

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +59
try {
if (serverInfo?.server?.listening) await serverInfo.server.close();
// will resolve to 127.0.0.1
const res = await app.curl(url + '/get_headers', { dataType: 'json' });
assert(res.status !== 200);
} catch (err: any) {
assert(err);
assert(err.message.includes('ECONNREFUSED'));
}
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The test logic appears flawed. After closing the server on line 52, the test expects a non-200 status or catches an ECONNREFUSED error. However, the assertion assert(res.status !== 200) on line 55 will never be reached if the connection is refused, as the curl call will throw an error immediately. The try-catch block makes the status check unreachable. Consider restructuring this test to only check for the expected error, or use a pattern that properly handles both success and error cases.

Suggested change
try {
if (serverInfo?.server?.listening) await serverInfo.server.close();
// will resolve to 127.0.0.1
const res = await app.curl(url + '/get_headers', { dataType: 'json' });
assert(res.status !== 200);
} catch (err: any) {
assert(err);
assert(err.message.includes('ECONNREFUSED'));
}
if (serverInfo?.server?.listening) await serverInfo.server.close();
// will resolve to 127.0.0.1 but should fail because server is down
await assert.rejects(
() => app.curl(url + '/get_headers', { dataType: 'json' }),
(err: any) => {
assert(err);
assert(err.message.includes('ECONNREFUSED'));
return true;
},
);

Copilot uses AI. Check for mistakes.
server: http.Server;
}> {
let localServer: http.Server;
return new Promise((resolve, reject) => {
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The return type Promise should have a more specific reject type. The Promise is initialized on line 162 but the error handling on lines 182-191 can reject with various error types (line 189 rejects with 'e' of type 'any'). Consider using a more specific error type or at least Error instead of the implicit unknown rejection type.

Copilot uses AI. Check for mistakes.
return resolve({ url, server: localServer });
};
localServer.listen(0, serverCallback);
localServer.on('error', (e: any) => {
Copy link

Copilot AI Dec 25, 2025

Choose a reason for hiding this comment

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

The error parameter has type 'any' which bypasses TypeScript's type safety. Consider using a more specific type such as NodeJS.ErrnoException or at minimum Error, as the code checks for e.code === 'EADDRINUSE' which is a property of NodeJS.ErrnoException.

Suggested change
localServer.on('error', (e: any) => {
localServer.on('error', (e: NodeJS.ErrnoException) => {

Copilot uses AI. Check for mistakes.
@fengmk2 fengmk2 self-assigned this Dec 25, 2025
@fengmk2 fengmk2 merged commit 715f67a into eggjs:next Dec 25, 2025
50 of 52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants