diff --git a/.eslintrc b/.eslintrc
index 466fc67d..9bcdb468 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -2,16 +2,5 @@
   "extends": [
     "eslint-config-egg/typescript",
     "eslint-config-egg/lib/rules/enforce-node-prefix"
-  ],
-  "parserOptions": {
-    // recommend to use another config file like tsconfig.eslint.json and extends tsconfig.json in it.
-    // because you may be need to lint test/**/*.test.ts but no need to emit to js.
-    // @see https://github.com/typescript-eslint/typescript-eslint/issues/890
-    "project": "./tsconfig.eslint.json"
-  },
-  "ignorePatterns": [
-    "src/**/*.js",
-    "src/esm",
-    "src/cjs"
   ]
 }
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
deleted file mode 100644
index 4285be1f..00000000
--- a/.github/workflows/codeql-analysis.yml
+++ /dev/null
@@ -1,70 +0,0 @@
-# For most projects, this workflow file will not need changing; you simply need
-# to commit it to your repository.
-#
-# You may wish to alter this file to override the set of languages analyzed,
-# or to provide custom queries or build logic.
-#
-# ******** NOTE ********
-# We have attempted to detect the languages in your repository. Please check
-# the `language` matrix defined below to confirm you have the correct set of
-# supported CodeQL languages.
-#
-name: "CodeQL"
-
-on:
-  push:
-    branches: [ "master" ]
-  pull_request:
-    # The branches below must be a subset of the branches above
-    branches: [ "master" ]
-
-jobs:
-  analyze:
-    name: Analyze
-    runs-on: ubuntu-latest
-    permissions:
-      actions: read
-      contents: read
-      security-events: write
-
-    strategy:
-      fail-fast: false
-      matrix:
-        language: [ 'javascript', 'typescript' ]
-        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
-        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
-
-    steps:
-    - name: Checkout repository
-      uses: actions/checkout@v4
-
-    # Initializes the CodeQL tools for scanning.
-    - name: Initialize CodeQL
-      uses: github/codeql-action/init@v3
-      with:
-        languages: ${{ matrix.language }}
-        # If you wish to specify custom queries, you can do so here or in a config file.
-        # By default, queries listed here will override any specified in a config file.
-        # Prefix the list here with "+" to use these queries and those in the config file.
-        
-        # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
-        # queries: security-extended,security-and-quality
-
-        
-    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
-    # If this step fails, then you should remove it and run the build manually (see below)
-    - name: Autobuild
-      uses: github/codeql-action/autobuild@v3
-
-    # ℹ️ Command-line programs to run using the OS shell.
-    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
-
-    #   If the Autobuild fails above, remove it and uncomment the following three lines. 
-    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
-
-    # - run: |
-    #   echo "Run, Build Application using script"
-    #   ./location_of_script_within_repo/buildscript.sh
-
-    - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index 9e51f0d3..271cd4b1 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -12,6 +12,6 @@ jobs:
     uses: node-modules/github-actions/.github/workflows/node-test.yml@master
     with:
       os: 'ubuntu-latest, macos-latest, windows-latest'
-      version: '14.19.3, 14, 16, 18, 20, 22'
+      version: '18.19.0, 20, 22'
     secrets:
       CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/README.md b/README.md
index ec5c9a63..48332177 100644
--- a/README.md
+++ b/README.md
@@ -283,25 +283,14 @@ Fork [undici benchmarks script](https://github.com/fengmk2/undici/blob/urllib-be
 | undici - stream     |     45 | 12523.45 req/sec | ± 2.97 % |             + 754.61 % |
 | undici - dispatch   |     51 | 12970.18 req/sec | ± 3.15 % |             + 785.10 % |
 
+## License
+
+[MIT](LICENSE)
 
 <!-- GITCONTRIBUTOR_START -->
 
 ## Contributors
 
-|[<img src="https://avatars.githubusercontent.com/u/156269?v=4" width="100px;"/><br/><sub><b>fengmk2</b></sub>](https://github.com/fengmk2)<br/>|[<img src="https://avatars.githubusercontent.com/u/985607?v=4" width="100px;"/><br/><sub><b>dead-horse</b></sub>](https://github.com/dead-horse)<br/>|[<img src="https://avatars.githubusercontent.com/u/32174276?v=4" width="100px;"/><br/><sub><b>semantic-release-bot</b></sub>](https://github.com/semantic-release-bot)<br/>|[<img src="https://avatars.githubusercontent.com/u/288288?v=4" width="100px;"/><br/><sub><b>xingrz</b></sub>](https://github.com/xingrz)<br/>|[<img src="https://avatars.githubusercontent.com/u/360661?v=4" width="100px;"/><br/><sub><b>popomore</b></sub>](https://github.com/popomore)<br/>|[<img src="https://avatars.githubusercontent.com/u/327019?v=4" width="100px;"/><br/><sub><b>JacksonTian</b></sub>](https://github.com/JacksonTian)<br/>|
-| :---: | :---: | :---: | :---: | :---: | :---: |
-|[<img src="https://avatars.githubusercontent.com/u/543405?v=4" width="100px;"/><br/><sub><b>ibigbug</b></sub>](https://github.com/ibigbug)<br/>|[<img src="https://avatars.githubusercontent.com/u/14790466?v=4" width="100px;"/><br/><sub><b>greenkeeperio-bot</b></sub>](https://github.com/greenkeeperio-bot)<br/>|[<img src="https://avatars.githubusercontent.com/u/227713?v=4" width="100px;"/><br/><sub><b>atian25</b></sub>](https://github.com/atian25)<br/>|[<img src="https://avatars.githubusercontent.com/u/6897780?v=4" width="100px;"/><br/><sub><b>killagu</b></sub>](https://github.com/killagu)<br/>|[<img src="https://avatars.githubusercontent.com/u/5381764?v=4" width="100px;"/><br/><sub><b>paambaati</b></sub>](https://github.com/paambaati)<br/>|[<img src="https://avatars.githubusercontent.com/u/199635?v=4" width="100px;"/><br/><sub><b>tremby</b></sub>](https://github.com/tremby)<br/>|
-|[<img src="https://avatars.githubusercontent.com/u/1433247?v=4" width="100px;"/><br/><sub><b>denghongcai</b></sub>](https://github.com/denghongcai)<br/>|[<img src="https://avatars.githubusercontent.com/u/4635838?v=4" width="100px;"/><br/><sub><b>gemwuu</b></sub>](https://github.com/gemwuu)<br/>|[<img src="https://avatars.githubusercontent.com/u/2842176?v=4" width="100px;"/><br/><sub><b>XadillaX</b></sub>](https://github.com/XadillaX)<br/>|[<img src="https://avatars.githubusercontent.com/u/1147375?v=4" width="100px;"/><br/><sub><b>alsotang</b></sub>](https://github.com/alsotang)<br/>|[<img src="https://avatars.githubusercontent.com/u/546535?v=4" width="100px;"/><br/><sub><b>leoner</b></sub>](https://github.com/leoner)<br/>|[<img src="https://avatars.githubusercontent.com/u/19908330?v=4" width="100px;"/><br/><sub><b>hyj1991</b></sub>](https://github.com/hyj1991)<br/>|
-|[<img src="https://avatars.githubusercontent.com/u/1747852?v=4" width="100px;"/><br/><sub><b>isayme</b></sub>](https://github.com/isayme)<br/>|[<img src="https://avatars.githubusercontent.com/u/252317?v=4" width="100px;"/><br/><sub><b>cyjake</b></sub>](https://github.com/cyjake)<br/>|[<img src="https://avatars.githubusercontent.com/u/5856440?v=4" width="100px;"/><br/><sub><b>whxaxes</b></sub>](https://github.com/whxaxes)<br/>|[<img src="https://avatars.githubusercontent.com/u/309219?v=4" width="100px;"/><br/><sub><b>chadxz</b></sub>](https://github.com/chadxz)<br/>|[<img src="https://avatars.githubusercontent.com/u/2055702?v=4" width="100px;"/><br/><sub><b>adapt0</b></sub>](https://github.com/adapt0)<br/>|[<img src="https://avatars.githubusercontent.com/u/5139554?v=4" width="100px;"/><br/><sub><b>danielwpz</b></sub>](https://github.com/danielwpz)<br/>|
-|[<img src="https://avatars.githubusercontent.com/u/5127897?v=4" width="100px;"/><br/><sub><b>danielsss</b></sub>](https://github.com/danielsss)<br/>|[<img src="https://avatars.githubusercontent.com/u/3367820?v=4" width="100px;"/><br/><sub><b>Jeff-Tian</b></sub>](https://github.com/Jeff-Tian)<br/>|[<img src="https://avatars.githubusercontent.com/u/17075261?v=4" width="100px;"/><br/><sub><b>nick-ng</b></sub>](https://github.com/nick-ng)<br/>|[<img src="https://avatars.githubusercontent.com/u/1706595?v=4" width="100px;"/><br/><sub><b>rishavsharan</b></sub>](https://github.com/rishavsharan)<br/>|[<img src="https://avatars.githubusercontent.com/u/1886161?v=4" width="100px;"/><br/><sub><b>willizm</b></sub>](https://github.com/willizm)<br/>|[<img src="https://avatars.githubusercontent.com/u/7227589?v=4" width="100px;"/><br/><sub><b>davidkhala</b></sub>](https://github.com/davidkhala)<br/>|
-|[<img src="https://avatars.githubusercontent.com/u/535479?v=4" width="100px;"/><br/><sub><b>aleafs</b></sub>](https://github.com/aleafs)<br/>|[<img src="https://avatars.githubusercontent.com/u/3689968?v=4" width="100px;"/><br/><sub><b>Amunu</b></sub>](https://github.com/Amunu)<br/>|[<img src="https://avatars.githubusercontent.com/in/9426?v=4" width="100px;"/><br/><sub><b>azure-pipelines[bot]</b></sub>](https://github.com/apps/azure-pipelines)<br/>|[<img src="https://avatars.githubusercontent.com/u/108602490?v=4" width="100px;"/><br/><sub><b>capsice</b></sub>](https://github.com/capsice)<br/>|[<img src="https://avatars.githubusercontent.com/u/1281323?v=4" width="100px;"/><br/><sub><b>changzhiwin</b></sub>](https://github.com/changzhiwin)<br/>|[<img src="https://avatars.githubusercontent.com/u/929503?v=4" width="100px;"/><br/><sub><b>yuzhigang33</b></sub>](https://github.com/yuzhigang33)<br/>|
-|[<img src="https://avatars.githubusercontent.com/u/5574625?v=4" width="100px;"/><br/><sub><b>elrrrrrrr</b></sub>](https://github.com/elrrrrrrr)<br/>|[<img src="https://avatars.githubusercontent.com/u/981128?v=4" width="100px;"/><br/><sub><b>fishbar</b></sub>](https://github.com/fishbar)<br/>|[<img src="https://avatars.githubusercontent.com/u/1207064?v=4" width="100px;"/><br/><sub><b>gxcsoccer</b></sub>](https://github.com/gxcsoccer)<br/>|[<img src="https://avatars.githubusercontent.com/u/17476119?v=4" width="100px;"/><br/><sub><b>mars-coder</b></sub>](https://github.com/mars-coder)<br/>|[<img src="https://avatars.githubusercontent.com/u/929179?v=4" width="100px;"/><br/><sub><b>rockdai</b></sub>](https://github.com/rockdai)<br/>|[<img src="https://avatars.githubusercontent.com/u/2196373?v=4" width="100px;"/><br/><sub><b>dickeylth</b></sub>](https://github.com/dickeylth)<br/>|
-[<img src="https://avatars.githubusercontent.com/u/13050025?v=4" width="100px;"/><br/><sub><b>aladdin-add</b></sub>](https://github.com/aladdin-add)<br/>
-
-This project follows the git-contributor [spec](https://github.com/xudafeng/git-contributor), auto updated at `Mon Dec 04 2023 00:13:39 GMT+0800`.
+[![Contributors](https://contrib.rocks/image?repo=node-modules/urllib)](https://github.com/node-modules/urllib/graphs/contributors)
 
-<!-- GITCONTRIBUTOR_END -->
-
-## License
-
-[MIT](LICENSE)
+Made with [contributors-img](https://contrib.rocks).
diff --git a/package.json b/package.json
index cecb688a..a1fff8c9 100644
--- a/package.json
+++ b/package.json
@@ -1,10 +1,10 @@
 {
   "name": "urllib",
-  "version": "3.25.1",
+  "version": "4.0.0",
   "publishConfig": {
     "tag": "latest"
   },
-  "description": "Help in opening URLs (mostly HTTP) in a complex world — basic and digest authentication, redirections, cookies and more. Base undici fetch API.",
+  "description": "Help in opening URLs (mostly HTTP) in a complex world — basic and digest authentication, redirections, timeout and more. Base undici API.",
   "keywords": [
     "urllib",
     "http",
@@ -39,42 +39,35 @@
     "test": "npm run lint && vitest run",
     "test-keepalive": "cross-env TEST_KEEPALIVE_COUNT=50 vitest run --test-timeout 180000 keep-alive-header.test.ts",
     "cov": "vitest run --coverage",
-    "preci": "node scripts/pre_test.js",
-    "ci": "npm run lint && npm run cov && node scripts/build_test.js",
-    "contributor": "git-contributor",
+    "ci": "npm run lint && npm run cov && npm run prepublishOnly && attw --pack",
     "clean": "rm -rf dist",
     "prepublishOnly": "npm run build"
   },
   "dependencies": {
-    "default-user-agent": "^1.0.0",
-    "digest-header": "^1.0.0",
-    "form-data-encoder": "^1.7.2",
-    "formdata-node": "^4.3.3",
-    "formstream": "^1.1.1",
+    "formstream": "^1.5.1",
     "mime-types": "^2.1.35",
-    "pump": "^3.0.0",
-    "qs": "^6.11.2",
-    "type-fest": "^4.3.1",
-    "undici": "^5.28.2",
-    "ylru": "^1.3.2"
+    "qs": "^6.12.1",
+    "type-fest": "^4.20.1",
+    "undici": "^6.19.2",
+    "ylru": "^2.0.0"
   },
   "devDependencies": {
+    "@arethetypeswrong/cli": "^0.15.3",
+    "@eggjs/tsconfig": "^1.3.3",
     "@tsconfig/node18": "^18.2.1",
     "@tsconfig/strictest": "^2.0.2",
     "@types/busboy": "^1.5.0",
-    "@types/default-user-agent": "^1.0.0",
     "@types/mime-types": "^2.1.1",
     "@types/node": "^20.2.1",
-    "@types/pump": "^1.1.1",
+    "@types/proxy": "^1.0.4",
     "@types/qs": "^6.9.7",
     "@types/selfsigned": "^2.0.1",
     "@types/tar-stream": "^2.2.2",
-    "@vitest/coverage-v8": "^1.3.1",
+    "@vitest/coverage-v8": "^1.6.0",
     "busboy": "^1.6.0",
     "cross-env": "^7.0.3",
-    "eslint": "^8.25.0",
-    "eslint-config-egg": "^12.1.0",
-    "git-contributor": "^2.0.0",
+    "eslint": "8",
+    "eslint-config-egg": "13",
     "iconv-lite": "^0.6.3",
     "proxy": "^1.0.2",
     "selfsigned": "^2.0.1",
@@ -82,10 +75,10 @@
     "tshy": "^1.0.0",
     "tshy-after": "^1.0.0",
     "typescript": "^5.0.4",
-    "vitest": "^1.3.1"
+    "vitest": "^1.6.0"
   },
   "engines": {
-    "node": ">= 14.19.3"
+    "node": ">= 18.19.0"
   },
   "license": "MIT",
   "type": "module",
@@ -98,10 +91,12 @@
   "exports": {
     ".": {
       "import": {
+        "source": "./src/index.ts",
         "types": "./dist/esm/index.d.ts",
         "default": "./dist/esm/index.js"
       },
       "require": {
+        "source": "./src/index.ts",
         "types": "./dist/commonjs/index.d.ts",
         "default": "./dist/commonjs/index.js"
       }
diff --git a/scripts/build_test.js b/scripts/build_test.js
deleted file mode 100644
index d2b06180..00000000
--- a/scripts/build_test.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import { execSync } from 'child_process';
-
-function main() {
-  if (process.version.startsWith('v14.')) {
-    console.log(`ignore build:test on Node.js ${process.version}`);
-    return;
-  }
-  const cwd = process.cwd()
-  execSync('npm run build:test', {
-    cwd,
-    stdio: [ 'inherit', 'inherit', 'inherit' ],
-  });
-}
-
-main();
diff --git a/scripts/pre_test.js b/scripts/pre_test.js
deleted file mode 100644
index f79e535d..00000000
--- a/scripts/pre_test.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { execSync } from 'node:child_process';
-import { writeFileSync, readFileSync } from 'node:fs';
-import { join } from 'node:path';
-
-function main() {
-  if (!process.version.startsWith('v14.')) {
-    return;
-  }
-  console.log(`use vitest@^0.33.0 on Node.js ${process.version}`);
-  const cwd = process.cwd()
-  execSync('npm i vitest@^0.33.0 @vitest/coverage-v8@^0.33.0', {
-    cwd,
-    stdio: [ 'inherit', 'inherit', 'inherit' ],
-  });
-  if (process.env.CI) {
-    // add --no-threads
-    const pkgFile = join(process.cwd(), 'package.json');
-    const pkg = JSON.parse(readFileSync(pkgFile, 'utf-8'));
-    pkg.scripts.cov = `${pkg.scripts.cov} --no-threads`;
-    writeFileSync(pkgFile, JSON.stringify(pkg));
-  }
-}
-
-main();
diff --git a/scripts/replace_urllib_version.js b/scripts/replace_urllib_version.js
index 0efb7b02..ee06f779 100644
--- a/scripts/replace_urllib_version.js
+++ b/scripts/replace_urllib_version.js
@@ -1,7 +1,7 @@
 #!/usr/bin/env node
 
-import fs from 'fs/promises';
-import path from 'path';
+import fs from 'node:fs/promises';
+import path from 'node:path';
 
 async function main() {
   const root = process.cwd();
@@ -12,9 +12,9 @@ async function main() {
   ];
   for (const file of files) {
     const content = await fs.readFile(file, 'utf-8');
-    // replace "('node-urllib', 'VERSION')" to "('node-urllib', 'pkg.version')"
-    const newContent = content.replace(/\(\'node-urllib\', \'VERSION\'\)/, (match) => {
-      const after = `('node-urllib', '${pkg.version}')`;
+    // replace "const VERSION = 'VERSION';" to "const VERSION = '4.0.0';"
+    const newContent = content.replace(/const VERSION = 'VERSION';/, match => {
+      const after = `const VERSION = '${pkg.version}';`;
       console.log('[%s] %s => %s', file, match, after);
       return after;
     });
diff --git a/src/HttpClient.ts b/src/HttpClient.ts
index 5bf5e7d2..dad15287 100644
--- a/src/HttpClient.ts
+++ b/src/HttpClient.ts
@@ -11,34 +11,33 @@ import {
 } from 'node:zlib';
 import { Blob } from 'node:buffer';
 import { Readable, pipeline } from 'node:stream';
-import stream from 'node:stream';
+import { pipeline as pipelinePromise } from 'node:stream/promises';
 import { basename } from 'node:path';
 import { createReadStream } from 'node:fs';
 import { format as urlFormat } from 'node:url';
 import { performance } from 'node:perf_hooks';
 import querystring from 'node:querystring';
+import { setTimeout as sleep } from 'node:timers/promises';
 import {
-  FormData as FormDataNative,
+  FormData,
   request as undiciRequest,
   Dispatcher,
   Agent,
   getGlobalDispatcher,
   Pool,
 } from 'undici';
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore
 import undiciSymbols from 'undici/lib/core/symbols.js';
-import { FormData as FormDataNode } from 'formdata-node';
-import { FormDataEncoder } from 'form-data-encoder';
-import createUserAgent from 'default-user-agent';
 import mime from 'mime-types';
 import qs from 'qs';
-import pump from 'pump';
 // Compatible with old style formstream
 import FormStream from 'formstream';
 import { HttpAgent, CheckAddressFunction } from './HttpAgent.js';
 import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
 import { RequestURL, RequestOptions, HttpMethod, RequestMeta } from './Request.js';
 import { RawResponseWithMeta, HttpClientResponse, SocketInfo } from './Response.js';
-import { parseJSON, sleep, digestAuthHeader, globalId, performanceTime, isReadable } from './utils.js';
+import { parseJSON, digestAuthHeader, globalId, performanceTime, isReadable } from './utils.js';
 import symbols from './symbols.js';
 import { initDiagnosticsChannel } from './diagnosticsChannel.js';
 import { HttpClientConnectTimeoutError, HttpClientRequestTimeoutError } from './HttpClientError.js';
@@ -49,24 +48,12 @@ type PropertyShouldBe<T, K extends keyof T, V> = Omit<T, K> & { [P in K]: V };
 type IUndiciRequestOption = PropertyShouldBe<UndiciRequestOption, 'headers', IncomingHttpHeaders>;
 
 const PROTO_RE = /^https?:\/\//i;
-const FormData = FormDataNative ?? FormDataNode;
-// impl promise pipeline on Node.js 14
-const pipelinePromise = stream.promises?.pipeline ?? function pipeline(...args: any[]) {
-  return new Promise<void>((resolve, reject) => {
-    pump(...args, (err?: Error) => {
-      if (err) return reject(err);
-      resolve();
-    });
-  });
-};
 
 function noop() {
   // noop
 }
 
 const debug = debuglog('urllib:HttpClient');
-// Node.js 14 or 16
-const isNode14Or16 = /v1[46]\./.test(process.version);
 
 export type ClientOptions = {
   defaultArgs?: RequestOptions;
@@ -125,7 +112,10 @@ class BlobFromStream {
   }
 }
 
-export const HEADER_USER_AGENT = createUserAgent('node-urllib', 'VERSION');
+export const VERSION = 'VERSION';
+// 'node-urllib/4.0.0 Node.js/18.19.0 (darwin; x64)'
+export const HEADER_USER_AGENT =
+  `node-urllib/${VERSION} Node.js/${process.version.substring(1)} (${process.platform}; ${process.arch})`;
 
 function getFileName(stream: Readable) {
   const filePath: string = (stream as any).path;
@@ -207,13 +197,13 @@ export class HttpClient extends EventEmitter {
   getDispatcherPoolStats() {
     const agent = this.getDispatcher();
     // origin => Pool Instance
-    const clients: Map<string, WeakRef<Pool>> | undefined = agent[undiciSymbols.kClients];
+    const clients: Map<string, WeakRef<Pool>> | undefined = Reflect.get(agent, undiciSymbols.kClients);
     const poolStatsMap: Record<string, PoolStat> = {};
     if (!clients) {
       return poolStatsMap;
     }
     for (const [ key, ref ] of clients) {
-      const pool = ref.deref();
+      const pool = typeof ref.deref === 'function' ? ref.deref() : ref as unknown as Pool;
       const stats = pool?.stats;
       if (!stats) continue;
       poolStatsMap[key] = {
@@ -451,9 +441,11 @@ export class HttpClient extends EventEmitter {
         } else if (typeof args.files === 'string' || Buffer.isBuffer(args.files)) {
           uploadFiles.push([ 'file', args.files ]);
         } else if (typeof args.files === 'object') {
-          for (const field in args.files) {
+          const files = args.files as Record<string, string | Readable | Buffer>;
+          for (const field in files) {
             // set custom fileName
-            uploadFiles.push([ field, args.files[field], field ]);
+            const file = files[field];
+            uploadFiles.push([ field, file, field ]);
           }
         }
         // set normal fields first
@@ -478,18 +470,7 @@ export class HttpClient extends EventEmitter {
             isStreamingRequest = true;
           }
         }
-
-        if (FormDataNative) {
-          requestOptions.body = formData;
-        } else {
-          // Node.js 14 does not support spec-compliant FormData
-          // https://github.com/octet-stream/form-data#usage
-          const encoder = new FormDataEncoder(formData as any);
-          Object.assign(headers, encoder.headers);
-          // fix "Content-Length":"NaN"
-          delete headers['Content-Length'];
-          requestOptions.body = Readable.from(encoder);
-        }
+        requestOptions.body = formData;
       } else if (args.content) {
         if (!isGETOrHEAD) {
           // handle content
@@ -507,7 +488,7 @@ export class HttpClient extends EventEmitter {
           || isReadable(args.data);
         if (isGETOrHEAD) {
           if (!isStringOrBufferOrReadable) {
-            let query;
+            let query: string;
             if (args.nestedQuerystring) {
               query = qs.stringify(args.data);
             } else {
@@ -608,9 +589,6 @@ export class HttpClient extends EventEmitter {
           res = Object.assign(response.body, res);
         }
       } else if (args.writeStream) {
-        if (isNode14Or16 && args.writeStream.destroyed) {
-          throw new Error('writeStream is destroyed');
-        }
         if (args.compressed === true && isCompressedContent) {
           const decoder = contentEncoding === 'gzip' ? createGunzip() : createBrotliDecompress();
           await pipelinePromise(response.body, decoder, args.writeStream);
diff --git a/src/diagnosticsChannel.ts b/src/diagnosticsChannel.ts
index 97780e00..38b8e2a4 100644
--- a/src/diagnosticsChannel.ts
+++ b/src/diagnosticsChannel.ts
@@ -20,15 +20,14 @@ let initedDiagnosticsChannel = false;
 //   -> undici:request:trailers => { request, trailers }
 
 function subscribe(name: string, listener: (message: unknown, channelName: string | symbol) => void) {
-  if (typeof diagnosticsChannel.subscribe === 'function') {
-    diagnosticsChannel.subscribe(name, listener);
-  } else {
-    // TODO: support Node.js 14, will be removed on the next major version
-    diagnosticsChannel.channel(name).subscribe(listener);
-  }
+  diagnosticsChannel.subscribe(name, listener);
 }
 
-function formatSocket(socket: Socket) {
+type SocketExtend = Socket & {
+  [key: symbol]: string | number | Date | undefined;
+};
+
+function formatSocket(socket: SocketExtend) {
   if (!socket) return socket;
   return {
     localAddress: socket[symbols.kSocketLocalAddress],
@@ -41,8 +40,7 @@ function formatSocket(socket: Socket) {
 }
 
 // make sure error contains socket info
-const kDestroy = Symbol('kDestroy');
-Socket.prototype[kDestroy] = Socket.prototype.destroy;
+const destroySocket = Socket.prototype.destroy;
 Socket.prototype.destroy = function(err?: any) {
   if (err) {
     Object.defineProperty(err, symbols.kErrorSocket, {
@@ -51,12 +49,12 @@ Socket.prototype.destroy = function(err?: any) {
       value: this,
     });
   }
-  return this[kDestroy](err);
+  return destroySocket.call(this, err);
 };
 
 function getRequestOpaque(request: DiagnosticsChannel.Request, kHandler?: symbol) {
   if (!kHandler) return;
-  const handler = request[kHandler];
+  const handler = Reflect.get(request, kHandler);
   // maxRedirects = 0 will get [Symbol(handler)]: RequestHandler {
   // responseHeaders: null,
   // opaque: {
@@ -70,7 +68,7 @@ function getRequestOpaque(request: DiagnosticsChannel.Request, kHandler?: symbol
 }
 
 export function initDiagnosticsChannel() {
-  // makre sure init global DiagnosticsChannel once
+  // make sure init global DiagnosticsChannel once
   if (initedDiagnosticsChannel) return;
   initedDiagnosticsChannel = true;
 
@@ -97,29 +95,27 @@ export function initDiagnosticsChannel() {
     opaque[symbols.kRequestTiming].queuing = performanceTime(opaque[symbols.kRequestStartTime]);
   });
 
-  // diagnosticsChannel.channel('undici:client:beforeConnect')
-
   subscribe('undici:client:connectError', (message, name) => {
-    const { error, connectParams } = message as DiagnosticsChannel.ClientConnectErrorMessage & { error: any };
-    let { socket } = message as DiagnosticsChannel.ClientConnectErrorMessage;
-    if (!socket && error[symbols.kErrorSocket]) {
-      socket = error[symbols.kErrorSocket];
+    const { error, connectParams, socket } = message as DiagnosticsChannel.ClientConnectErrorMessage & { error: any, socket: SocketExtend };
+    let sock = socket;
+    if (!sock && error[symbols.kErrorSocket]) {
+      sock = error[symbols.kErrorSocket];
     }
-    if (socket) {
-      socket[symbols.kSocketId] = globalId('UndiciSocket');
-      socket[symbols.kSocketConnectErrorTime] = new Date();
-      socket[symbols.kHandledRequests] = 0;
-      socket[symbols.kHandledResponses] = 0;
+    if (sock) {
+      sock[symbols.kSocketId] = globalId('UndiciSocket');
+      sock[symbols.kSocketConnectErrorTime] = new Date();
+      sock[symbols.kHandledRequests] = 0;
+      sock[symbols.kHandledResponses] = 0;
       // copy local address to symbol, avoid them be reset after request error throw
-      if (socket.localAddress) {
-        socket[symbols.kSocketLocalAddress] = socket.localAddress;
-        socket[symbols.kSocketLocalPort] = socket.localPort;
+      if (sock.localAddress) {
+        sock[symbols.kSocketLocalAddress] = sock.localAddress;
+        sock[symbols.kSocketLocalPort] = sock.localPort;
       }
-      socket[symbols.kSocketConnectProtocol] = connectParams.protocol;
-      socket[symbols.kSocketConnectHost] = connectParams.host;
-      socket[symbols.kSocketConnectPort] = connectParams.port;
+      sock[symbols.kSocketConnectProtocol] = connectParams.protocol;
+      sock[symbols.kSocketConnectHost] = connectParams.host;
+      sock[symbols.kSocketConnectPort] = connectParams.port;
       debug('[%s] Socket#%d connectError, connectParams: %o, error: %s, (sock: %o)',
-        name, socket[symbols.kSocketId], connectParams, (error as Error).message, formatSocket(socket));
+        name, sock[symbols.kSocketId], connectParams, (error as Error).message, formatSocket(sock));
     } else {
       debug('[%s] connectError, connectParams: %o, error: %o',
         name, connectParams, error);
@@ -128,7 +124,7 @@ export function initDiagnosticsChannel() {
 
   // This message is published after a connection is established.
   subscribe('undici:client:connected', (message, name) => {
-    const { socket, connectParams } = message as DiagnosticsChannel.ClientConnectedMessage;
+    const { socket, connectParams } = message as DiagnosticsChannel.ClientConnectedMessage & { socket: SocketExtend };
     socket[symbols.kSocketId] = globalId('UndiciSocket');
     socket[symbols.kSocketStartTime] = performance.now();
     socket[symbols.kSocketConnectedTime] = new Date();
@@ -145,11 +141,11 @@ export function initDiagnosticsChannel() {
 
   // This message is published right before the first byte of the request is written to the socket.
   subscribe('undici:client:sendHeaders', (message, name) => {
-    const { request, socket } = message as DiagnosticsChannel.ClientSendHeadersMessage;
+    const { request, socket } = message as DiagnosticsChannel.ClientSendHeadersMessage & { socket: SocketExtend };
     const opaque = getRequestOpaque(request, kHandler);
     if (!opaque || !opaque[symbols.kRequestId]) return;
 
-    socket[symbols.kHandledRequests]++;
+    (socket[symbols.kHandledRequests] as number)++;
     // attach socket to opaque
     opaque[symbols.kRequestSocket] = socket;
     debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)',
@@ -158,11 +154,11 @@ export function initDiagnosticsChannel() {
 
     if (!opaque[symbols.kEnableRequestTiming]) return;
     opaque[symbols.kRequestTiming].requestHeadersSent = performanceTime(opaque[symbols.kRequestStartTime]);
-    // first socket need to caculate the connected time
+    // first socket need to calculate the connected time
     if (socket[symbols.kHandledRequests] === 1) {
       // kSocketStartTime - kRequestStartTime = connected time
       opaque[symbols.kRequestTiming].connected =
-        performanceTime(opaque[symbols.kRequestStartTime], socket[symbols.kSocketStartTime]);
+        performanceTime(opaque[symbols.kRequestStartTime], socket[symbols.kSocketStartTime] as number);
     }
   });
 
diff --git a/src/index.ts b/src/index.ts
index cc3cec4d..6cbf7d1c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,4 +1,4 @@
-import LRU from 'ylru';
+import { LRU } from 'ylru';
 import { HttpClient, HEADER_USER_AGENT } from './HttpClient.js';
 import { RequestOptions, RequestURL } from './Request.js';
 
diff --git a/src/utils.ts b/src/utils.ts
index bde49674..1f24c443 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -3,7 +3,7 @@ import { Readable } from 'node:stream';
 import { performance } from 'node:perf_hooks';
 import type { FixJSONCtlChars } from './Request.js';
 
-const JSONCtlCharsMap = {
+const JSONCtlCharsMap: Record<string, string> = {
   '"': '\\"', // \u0022
   '\\': '\\\\', // \u005c
   '\b': '\\b', // \u0008
@@ -49,12 +49,6 @@ export function parseJSON(data: string, fixJSONCtlChars?: FixJSONCtlChars) {
   return data;
 }
 
-export function sleep(ms: number) {
-  return new Promise<void>(resolve => {
-    setTimeout(resolve, ms);
-  });
-}
-
 function md5(s: string) {
   const sum = createHash('md5');
   sum.update(s, 'utf8');
diff --git a/test/HttpClient.connect.rejectUnauthorized.test.ts b/test/HttpClient.connect.rejectUnauthorized.test.ts
index 47591c8b..f28192ad 100644
--- a/test/HttpClient.connect.rejectUnauthorized.test.ts
+++ b/test/HttpClient.connect.rejectUnauthorized.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import { HttpClient } from '../src';
-import { startServer } from './fixtures/server';
+import { HttpClient } from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('HttpClient.connect.rejectUnauthorized.test.ts', () => {
   let close: any;
diff --git a/test/HttpClient.events.test.ts b/test/HttpClient.events.test.ts
index b1c4ce78..fb434c7c 100644
--- a/test/HttpClient.events.test.ts
+++ b/test/HttpClient.events.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import { HttpClient } from '../src';
-import { startServer } from './fixtures/server';
+import { HttpClient } from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('HttpClient.events.test.ts', () => {
   let close: any;
diff --git a/test/HttpClient.test.ts b/test/HttpClient.test.ts
index 07ecdacc..c9e02825 100644
--- a/test/HttpClient.test.ts
+++ b/test/HttpClient.test.ts
@@ -1,9 +1,8 @@
 import { strict as assert } from 'node:assert';
 import dns from 'node:dns';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import { HttpClient } from '../src';
-import { RawResponseWithMeta } from '../src/Response';
-import { startServer } from './fixtures/server';
+import { HttpClient, RawResponseWithMeta } from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('HttpClient.test.ts', () => {
   let close: any;
diff --git a/test/diagnostics_channel.test.ts b/test/diagnostics_channel.test.ts
index a31f9f8b..f3796494 100644
--- a/test/diagnostics_channel.test.ts
+++ b/test/diagnostics_channel.test.ts
@@ -1,14 +1,14 @@
 import { strict as assert } from 'node:assert';
 import diagnosticsChannel from 'node:diagnostics_channel';
+import { setTimeout as sleep } from 'node:timers/promises';
 import { describe, it, beforeEach, afterEach } from 'vitest';
-import urllib from '../src';
+import urllib from '../src/index.js';
 import type {
   RequestDiagnosticsMessage,
   ResponseDiagnosticsMessage,
-} from '../src';
-import symbols from '../src/symbols';
-import { startServer } from './fixtures/server';
-import { sleep } from './utils';
+} from '../src/index.js';
+import symbols from '../src/symbols.js';
+import { startServer } from './fixtures/server.js';
 
 describe('diagnostics_channel.test.ts', () => {
   let close: any;
@@ -62,9 +62,9 @@ describe('diagnostics_channel.test.ts', () => {
       lastRequestOpaque = opaque;
       // console.log(request);
     }
-    diagnosticsChannel.channel('undici:client:connected').subscribe(onMessage);
-    diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(onMessage);
-    diagnosticsChannel.channel('undici:request:trailers').subscribe(onMessage);
+    diagnosticsChannel.subscribe('undici:client:connected', onMessage);
+    diagnosticsChannel.subscribe('undici:client:sendHeaders', onMessage);
+    diagnosticsChannel.subscribe('undici:request:trailers', onMessage);
 
     let traceId = `mock-traceid-${Date.now()}`;
     // _url = 'https://registry.npmmirror.com/';
@@ -130,9 +130,9 @@ describe('diagnostics_channel.test.ts', () => {
       assert.equal(lastRequestOpaque.tracer.socket.requests, 2 + 1000 - count);
     }
 
-    diagnosticsChannel.channel('undici:client:connected').unsubscribe(onMessage);
-    diagnosticsChannel.channel('undici:client:sendHeaders').unsubscribe(onMessage);
-    diagnosticsChannel.channel('undici:request:trailers').unsubscribe(onMessage);
+    diagnosticsChannel.unsubscribe('undici:client:connected', onMessage);
+    diagnosticsChannel.unsubscribe('undici:client:sendHeaders', onMessage);
+    diagnosticsChannel.unsubscribe('undici:request:trailers', onMessage);
   });
 
   it('should support trace request by urllib:request and urllib:response', async () => {
@@ -147,8 +147,8 @@ describe('diagnostics_channel.test.ts', () => {
       socket = response.socket;
       assert.equal(request.args.opaque, lastRequestOpaque);
     }
-    diagnosticsChannel.channel('urllib:request').subscribe(onRequestMessage);
-    diagnosticsChannel.channel('urllib:response').subscribe(onResponseMessage);
+    diagnosticsChannel.subscribe('urllib:request', onRequestMessage);
+    diagnosticsChannel.subscribe('urllib:response', onResponseMessage);
 
     let traceId = `mock-traceid-${Date.now()}`;
     // _url = 'https://registry.npmmirror.com/';
@@ -218,8 +218,8 @@ describe('diagnostics_channel.test.ts', () => {
       assert.equal(socket.handledResponses, 2 + 1000 - count);
     }
 
-    diagnosticsChannel.channel('urllib:request').unsubscribe(onRequestMessage);
-    diagnosticsChannel.channel('urllib:response').unsubscribe(onResponseMessage);
+    diagnosticsChannel.unsubscribe('urllib:request', onRequestMessage);
+    diagnosticsChannel.unsubscribe('urllib:response', onResponseMessage);
   });
 
   it('should support trace request error by urllib:request and urllib:response', async () => {
@@ -236,8 +236,8 @@ describe('diagnostics_channel.test.ts', () => {
       assert.equal(request.args.opaque, lastRequestOpaque);
       lastError = error;
     }
-    diagnosticsChannel.channel('urllib:request').subscribe(onRequestMessage);
-    diagnosticsChannel.channel('urllib:response').subscribe(onResponseMessage);
+    diagnosticsChannel.subscribe('urllib:request', onRequestMessage);
+    diagnosticsChannel.subscribe('urllib:response', onResponseMessage);
 
     let traceId = `mock-traceid-${Date.now()}`;
     // handle network error
@@ -311,7 +311,7 @@ describe('diagnostics_channel.test.ts', () => {
     assert.equal(socket.handledRequests, 2);
     assert.equal(socket.handledResponses, 2);
 
-    diagnosticsChannel.channel('urllib:request').unsubscribe(onRequestMessage);
-    diagnosticsChannel.channel('urllib:response').unsubscribe(onResponseMessage);
+    diagnosticsChannel.unsubscribe('urllib:request', onRequestMessage);
+    diagnosticsChannel.unsubscribe('urllib:response', onResponseMessage);
   });
 });
diff --git a/test/fixtures/server.ts b/test/fixtures/server.ts
index 1e62bb6e..6bdd593b 100644
--- a/test/fixtures/server.ts
+++ b/test/fixtures/server.ts
@@ -3,11 +3,12 @@ import { createServer, Server, IncomingMessage, ServerResponse } from 'node:http
 import { createServer as createHttpsServer } from 'node:https';
 import { createBrotliCompress, createGzip, gzipSync, brotliCompressSync } from 'node:zlib';
 import { createReadStream } from 'node:fs';
+import { setTimeout as sleep } from 'node:timers/promises';
 import busboy from 'busboy';
 import iconv from 'iconv-lite';
 import selfsigned from 'selfsigned';
 import qs from 'qs';
-import { readableToBytes, sleep } from '../utils';
+import { readableToBytes } from '../utils.js';
 
 const requestsPerSocket = Symbol('requestsPerSocket');
 
diff --git a/test/index.test.ts b/test/index.test.ts
index 90a677c0..9a403c2f 100644
--- a/test/index.test.ts
+++ b/test/index.test.ts
@@ -2,10 +2,12 @@ import { strict as assert } from 'node:assert';
 import { parse as urlparse } from 'node:url';
 import { readFileSync } from 'node:fs';
 import { describe, it, beforeAll, afterAll, afterEach, beforeEach } from 'vitest';
-import urllib, { HttpClient, getDefaultHttpClient } from '../src';
-import { MockAgent, setGlobalDispatcher, getGlobalDispatcher } from '../src';
-import { startServer } from './fixtures/server';
-import { readableToBytes } from './utils';
+import urllib, {
+  HttpClient, getDefaultHttpClient,
+  MockAgent, setGlobalDispatcher, getGlobalDispatcher,
+} from '../src/index.js';
+import { startServer } from './fixtures/server.js';
+import { readableToBytes } from './utils.js';
 
 describe('index.test.ts', () => {
   let close: any;
diff --git a/test/keep-alive-header.test.ts b/test/keep-alive-header.test.ts
index 3d797062..83e1d620 100644
--- a/test/keep-alive-header.test.ts
+++ b/test/keep-alive-header.test.ts
@@ -1,8 +1,9 @@
 import { strict as assert } from 'node:assert';
+import { setTimeout as sleep } from 'node:timers/promises';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import { HttpClient } from '../src';
-import { startServer } from './fixtures/server';
-import { isWindows, nodeMajorVersion, sleep } from './utils';
+import { HttpClient } from '../src/index.js';
+import { startServer } from './fixtures/server.js';
+import { isWindows } from './utils.js';
 
 describe('keep-alive-header.test.ts', () => {
   // should shorter than server keepalive timeout
@@ -33,8 +34,8 @@ describe('keep-alive-header.test.ts', () => {
         const task = httpClient.request(_url);
         // console.log('after request stats: %o', httpClient.getDispatcherPoolStats());
         if (httpClient.getDispatcherPoolStats()[origin]) {
-          if (!(nodeMajorVersion() === 14 || isWindows())) {
-            // ignore node = 14 & windows
+          if (!isWindows()) {
+            // ignore on windows
             assert.equal(httpClient.getDispatcherPoolStats()[origin].pending, 1);
             assert.equal(httpClient.getDispatcherPoolStats()[origin].size, 1);
           }
@@ -42,7 +43,8 @@ describe('keep-alive-header.test.ts', () => {
         let response = await task;
         // console.log('after response stats: %o', httpClient.getDispatcherPoolStats());
         assert.equal(httpClient.getDispatcherPoolStats()[origin].pending, 0);
-        assert.equal(httpClient.getDispatcherPoolStats()[origin].connected, 1);
+        // assert.equal(httpClient.getDispatcherPoolStats()[origin].connected, 1);
+        assert.equal(httpClient.getDispatcherPoolStats()[origin].connected, 0);
         // console.log(response.res.socket);
         assert.equal(response.status, 200);
         // console.log(response.headers);
@@ -84,7 +86,7 @@ describe('keep-alive-header.test.ts', () => {
         // console.log(response.headers);
         assert.equal(response.headers.connection, 'keep-alive');
         assert.equal(response.headers['keep-alive'], 'timeout=2');
-        assert(parseInt(response.headers['x-requests-persocket'] as string) > 1);
+        assert(parseInt(response.headers['x-requests-persocket'] as string) >= 1, response.headers['x-requests-persocket'] as string);
         await sleep(keepAliveTimeout / 2);
         response = await httpClient.request(_url);
         // console.log(response.res.socket);
@@ -128,11 +130,13 @@ describe('keep-alive-header.test.ts', () => {
         // console.log(response.headers);
         assert.equal(response.headers.connection, 'keep-alive');
         assert.equal(response.headers['keep-alive'], 'timeout=2');
-        assert(parseInt(response.headers['x-requests-persocket'] as string) > 1);
+        assert(parseInt(response.headers['x-requests-persocket'] as string) >= 1, response.headers['x-requests-persocket'] as string);
         // console.log('before sleep stats: %o', httpClient.getDispatcherPoolStats());
         // { connected: 2, free: 1, pending: 0, queued: 0, running: 0, size: 0 }
-        assert.equal(httpClient.getDispatcherPoolStats()[origin].connected, 2);
-        assert.equal(httpClient.getDispatcherPoolStats()[origin].free, 1);
+        // assert.equal(httpClient.getDispatcherPoolStats()[origin].connected, 2);
+        assert.equal(httpClient.getDispatcherPoolStats()[origin].connected, 0);
+        // assert.equal(httpClient.getDispatcherPoolStats()[origin].free, 1);
+        assert.equal(httpClient.getDispatcherPoolStats()[origin].free, 0);
         await sleep(keepAliveTimeout);
         // console.log('after sleep stats: %o', httpClient.getDispatcherPoolStats());
         // clients maybe all gone => after sleep stats: {}
@@ -144,7 +148,7 @@ describe('keep-alive-header.test.ts', () => {
           assert(httpClient.getDispatcherPoolStats()[origin].free <= 2);
           assert.equal(httpClient.getDispatcherPoolStats()[origin].size, 0);
         }
-      } catch (err) {
+      } catch (err: any) {
         if (err.message === 'other side closed') {
           console.log(err);
           otherSideClosed++;
diff --git a/test/non-ascii-request-header.test.ts b/test/non-ascii-request-header.test.ts
index e7073f95..1517dd25 100644
--- a/test/non-ascii-request-header.test.ts
+++ b/test/non-ascii-request-header.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 // https://github.com/node-modules/urllib/issues/198
 describe('non-ascii-request-header.test.ts', () => {
diff --git a/test/options.auth.test.ts b/test/options.auth.test.ts
index aaeb8ea1..93b2bf65 100644
--- a/test/options.auth.test.ts
+++ b/test/options.auth.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.auth.test.ts', () => {
   let close: any;
diff --git a/test/options.compressed.test.ts b/test/options.compressed.test.ts
index 1a17ad3b..ae64e486 100644
--- a/test/options.compressed.test.ts
+++ b/test/options.compressed.test.ts
@@ -1,9 +1,9 @@
 import { strict as assert } from 'node:assert';
 import { createWriteStream, createReadStream } from 'node:fs';
 import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
-import { readableToString, createTempfile, nodeMajorVersion } from './utils';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
+import { readableToString, createTempfile, nodeMajorVersion } from './utils.js';
 
 describe('options.compressed.test.ts', () => {
   let close: any;
diff --git a/test/options.content.test.ts b/test/options.content.test.ts
index d8399522..0bcf9a8d 100644
--- a/test/options.content.test.ts
+++ b/test/options.content.test.ts
@@ -2,8 +2,8 @@ import { strict as assert } from 'node:assert';
 import { createReadStream } from 'node:fs';
 import { describe, it, beforeAll, afterAll } from 'vitest';
 import fs from 'node:fs/promises';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.content.test.ts', () => {
   let close: any;
diff --git a/test/options.data.test.ts b/test/options.data.test.ts
index f804bc7f..55dd5508 100644
--- a/test/options.data.test.ts
+++ b/test/options.data.test.ts
@@ -3,8 +3,8 @@ import { createReadStream } from 'node:fs';
 import { Readable } from 'node:stream';
 import qs from 'qs';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.data.test.ts', () => {
   let close: any;
diff --git a/test/options.dataType.test.ts b/test/options.dataType.test.ts
index 725b7a58..36f32c1b 100644
--- a/test/options.dataType.test.ts
+++ b/test/options.dataType.test.ts
@@ -1,8 +1,8 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
-import { nodeMajorVersion, readableToBytes } from './utils';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
+import { nodeMajorVersion, readableToBytes } from './utils.js';
 
 describe('options.dataType.test.ts', () => {
   let close: any;
diff --git a/test/options.digestAuth.test.ts b/test/options.digestAuth.test.ts
index 463c9b61..e9385a3d 100644
--- a/test/options.digestAuth.test.ts
+++ b/test/options.digestAuth.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.digestAuth.test.ts', () => {
   let close: any;
diff --git a/test/options.dispatcher.test.ts b/test/options.dispatcher.test.ts
index bde1e00e..ea133064 100644
--- a/test/options.dispatcher.test.ts
+++ b/test/options.dispatcher.test.ts
@@ -1,8 +1,8 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
 import setup from 'proxy';
-import { request, ProxyAgent, getGlobalDispatcher, setGlobalDispatcher, Agent } from '../src';
-import { startServer } from './fixtures/server';
+import { request, ProxyAgent, getGlobalDispatcher, setGlobalDispatcher, Agent } from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.dispatcher.test.ts', () => {
   let close: any;
diff --git a/test/options.files.test.ts b/test/options.files.test.ts
index 0550b5fc..60739d09 100644
--- a/test/options.files.test.ts
+++ b/test/options.files.test.ts
@@ -4,8 +4,8 @@ import fs from 'node:fs/promises';
 import { createReadStream } from 'node:fs';
 import { Readable } from 'node:stream';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.files.test.ts', () => {
   let close: any;
@@ -274,7 +274,8 @@ describe('options.files.test.ts', () => {
     const response = await urllib.request(`${_url}multipart`, {
       files: {
         'buffer.js': Buffer.from(rawData),
-        'readable.js': Readable.from([ rawData ]),
+        // Readable.from data must be Buffer or Bytes
+        'readable.js': Readable.from([ Buffer.from(rawData) ]),
       },
       data: {
         hello: 'hello world,😄😓',
diff --git a/test/options.fixJSONCtlChars.test.ts b/test/options.fixJSONCtlChars.test.ts
index 381362e3..54b84849 100644
--- a/test/options.fixJSONCtlChars.test.ts
+++ b/test/options.fixJSONCtlChars.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.fixJSONCtlChars.test.ts', () => {
   let close: any;
diff --git a/test/options.followRedirect.test.ts b/test/options.followRedirect.test.ts
index f001101b..1a399751 100644
--- a/test/options.followRedirect.test.ts
+++ b/test/options.followRedirect.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.followRedirect.test.js', () => {
   let close: any;
diff --git a/test/options.gzip.test.ts b/test/options.gzip.test.ts
index 6f30d43e..292c9f35 100644
--- a/test/options.gzip.test.ts
+++ b/test/options.gzip.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.gzip.test.ts', () => {
   let close: any;
diff --git a/test/options.headers.test.ts b/test/options.headers.test.ts
index 0b3e8ad1..12acfacc 100644
--- a/test/options.headers.test.ts
+++ b/test/options.headers.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.headers.test.ts', () => {
   let close: any;
diff --git a/test/options.method.test.ts b/test/options.method.test.ts
index 6590d162..a3adadb7 100644
--- a/test/options.method.test.ts
+++ b/test/options.method.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.method.test.ts', () => {
   let close: any;
diff --git a/test/options.opaque.test.ts b/test/options.opaque.test.ts
index 02d3287d..eccecc0a 100644
--- a/test/options.opaque.test.ts
+++ b/test/options.opaque.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.opaque.test.ts', () => {
   let close: any;
diff --git a/test/options.reset.test.ts b/test/options.reset.test.ts
index 889ffcd0..590bd590 100644
--- a/test/options.reset.test.ts
+++ b/test/options.reset.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.reset.test.ts', () => {
   let close: any;
diff --git a/test/options.retry.test.ts b/test/options.retry.test.ts
index 8313e64c..1b52f7bf 100644
--- a/test/options.retry.test.ts
+++ b/test/options.retry.test.ts
@@ -1,9 +1,9 @@
 import { strict as assert } from 'node:assert';
 import { createWriteStream, createReadStream } from 'node:fs';
 import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
-import { readableToString, createTempfile } from './utils';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
+import { readableToString, createTempfile } from './utils.js';
 
 describe('options.retry.test.ts', () => {
   let close: any;
diff --git a/test/options.signal.test.ts b/test/options.signal.test.ts
index 3f2d4b85..6c67a8f1 100644
--- a/test/options.signal.test.ts
+++ b/test/options.signal.test.ts
@@ -1,9 +1,9 @@
 import { strict as assert } from 'node:assert';
 import { EventEmitter } from 'node:events';
+import { setTimeout as sleep } from 'node:timers/promises';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
-import { sleep } from './utils';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.signal.test.ts', () => {
   let close: any;
@@ -18,7 +18,7 @@ describe('options.signal.test.ts', () => {
     await close();
   });
 
-  it.skipIf(typeof global.AbortController === 'undefined')('should throw error when AbortController abort', async () => {
+  it('should throw error when AbortController abort', async () => {
     await assert.rejects(async () => {
       const abortController = new AbortController();
       const p = urllib.request(`${_url}?timeout=2000`, {
@@ -28,10 +28,9 @@ describe('options.signal.test.ts', () => {
       abortController.abort();
       await p;
     }, (err: any) => {
-      // console.error(err);
       assert.equal(err.name, 'AbortError');
-      assert.equal(err.message, 'Request aborted');
-      assert.equal(err.code, 'UND_ERR_ABORTED');
+      assert.equal(err.message, 'This operation was aborted');
+      assert.equal(err.code, 20);
       return true;
     });
   });
diff --git a/test/options.socketErrorRetry.test.ts b/test/options.socketErrorRetry.test.ts
index 1006241d..5831785c 100644
--- a/test/options.socketErrorRetry.test.ts
+++ b/test/options.socketErrorRetry.test.ts
@@ -1,9 +1,9 @@
 import { strict as assert } from 'node:assert';
 import { createWriteStream, createReadStream } from 'node:fs';
 import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
-import { createTempfile } from './utils';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
+import { createTempfile } from './utils.js';
 
 describe('options.socketErrorRetry.test.ts', () => {
   let close: any;
diff --git a/test/options.socketPath.test.ts b/test/options.socketPath.test.ts
index c696222a..fc6bcf3c 100644
--- a/test/options.socketPath.test.ts
+++ b/test/options.socketPath.test.ts
@@ -1,8 +1,8 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/socket_server';
-import { isWindows } from './utils';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/socket_server.js';
+import { isWindows } from './utils.js';
 
 describe.skipIf(isWindows())('options.socketPath.test.ts', () => {
   let close: any;
diff --git a/test/options.stream.test.ts b/test/options.stream.test.ts
index 17a5f681..2c905488 100644
--- a/test/options.stream.test.ts
+++ b/test/options.stream.test.ts
@@ -7,10 +7,10 @@ import { Readable } from 'node:stream';
 import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
 import tar from 'tar-stream';
 import FormStream from 'formstream';
-import urllib from '../src';
-import { isReadable } from '../src/utils';
-import { startServer } from './fixtures/server';
-import { createTempfile } from './utils';
+import urllib from '../src/index.js';
+import { isReadable } from '../src/utils.js';
+import { startServer } from './fixtures/server.js';
+import { createTempfile } from './utils.js';
 
 describe('options.stream.test.ts', () => {
   let close: any;
diff --git a/test/options.streaming.test.ts b/test/options.streaming.test.ts
index a98f998a..f0e97dac 100644
--- a/test/options.streaming.test.ts
+++ b/test/options.streaming.test.ts
@@ -2,10 +2,10 @@ import { strict as assert } from 'node:assert';
 import { pipeline } from 'node:stream';
 import { createBrotliDecompress } from 'node:zlib';
 import { describe, it, beforeEach, afterEach } from 'vitest';
-import urllib from '../src';
-import { isReadable } from '../src/utils';
-import { startServer } from './fixtures/server';
-import { readableToBytes } from './utils';
+import urllib from '../src/index.js';
+import { isReadable } from '../src/utils.js';
+import { startServer } from './fixtures/server.js';
+import { readableToBytes } from './utils.js';
 
 describe('options.streaming.test.ts', () => {
   let close: any;
diff --git a/test/options.timeout.test.ts b/test/options.timeout.test.ts
index d5e9d521..b065096b 100644
--- a/test/options.timeout.test.ts
+++ b/test/options.timeout.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib, { HttpClientRequestTimeoutError } from '../src';
-import { startServer } from './fixtures/server';
+import urllib, { HttpClientRequestTimeoutError } from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.timeout.test.ts', () => {
   let close: any;
@@ -25,12 +25,9 @@ describe('options.timeout.test.ts', () => {
       // console.error(err);
       assert.equal(err.name, 'HttpClientRequestTimeoutError');
       assert.equal(err.message, 'Request timeout for 10 ms');
-      if (err.cause) {
-        // not work on Node.js 14
-        assert.equal(err.cause.name, 'HeadersTimeoutError');
-        assert.equal(err.cause.message, 'Headers Timeout Error');
-        assert.equal(err.cause.code, 'UND_ERR_HEADERS_TIMEOUT');
-      }
+      assert.equal(err.cause.name, 'HeadersTimeoutError');
+      assert.equal(err.cause.message, 'Headers Timeout Error');
+      assert.equal(err.cause.code, 'UND_ERR_HEADERS_TIMEOUT');
 
       assert.equal(err.res.status, -1);
       assert(err.res.rt > 10, `actual ${err.res.rt}`);
diff --git a/test/options.timing.test.ts b/test/options.timing.test.ts
index c6d2ca41..680686c2 100644
--- a/test/options.timing.test.ts
+++ b/test/options.timing.test.ts
@@ -1,9 +1,8 @@
 import { strict as assert } from 'node:assert';
+import { setTimeout as sleep } from 'node:timers/promises';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { RawResponseWithMeta } from '../src/Response';
-import { startServer } from './fixtures/server';
-import { sleep } from './utils';
+import urllib, { RawResponseWithMeta } from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.timing.test.ts', () => {
   let close: any;
diff --git a/test/options.type.test.ts b/test/options.type.test.ts
index 91ffaabc..7bbf3049 100644
--- a/test/options.type.test.ts
+++ b/test/options.type.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('options.type.test.ts', () => {
   let close: any;
diff --git a/test/options.writeStream.test.ts b/test/options.writeStream.test.ts
index 42cbfc20..bc17e41d 100644
--- a/test/options.writeStream.test.ts
+++ b/test/options.writeStream.test.ts
@@ -3,10 +3,11 @@ import { createWriteStream } from 'node:fs';
 import { join } from 'node:path';
 import { gunzipSync } from 'node:zlib';
 import { stat, readFile } from 'node:fs/promises';
+import { setTimeout as sleep } from 'node:timers/promises';
 import { describe, it, beforeAll, afterAll, beforeEach, afterEach } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
-import { createTempfile, sleep } from './utils';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
+import { createTempfile } from './utils.js';
 
 describe('options.writeStream.test.ts', () => {
   let close: any;
diff --git a/test/response-charset-gbk.test.ts b/test/response-charset-gbk.test.ts
index c6735da1..3c929754 100644
--- a/test/response-charset-gbk.test.ts
+++ b/test/response-charset-gbk.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('response-charset-gbk.test.ts', () => {
   let close: any;
diff --git a/test/user-agent.test.ts b/test/user-agent.test.ts
index 61e489dd..3d869e0b 100644
--- a/test/user-agent.test.ts
+++ b/test/user-agent.test.ts
@@ -1,7 +1,7 @@
 import { strict as assert } from 'node:assert';
 import { describe, it, beforeAll, afterAll } from 'vitest';
-import urllib from '../src';
-import { startServer } from './fixtures/server';
+import urllib from '../src/index.js';
+import { startServer } from './fixtures/server.js';
 
 describe('keep-alive-header.test.ts', () => {
   let close: any;
diff --git a/test/utils.test.ts b/test/utils.test.ts
index 92ba28a6..8d7f7ec0 100644
--- a/test/utils.test.ts
+++ b/test/utils.test.ts
@@ -1,6 +1,6 @@
 import { strict as assert } from 'node:assert';
 import { describe, it } from 'vitest';
-import { globalId } from '../src/utils';
+import { globalId } from '../src/utils.js';
 
 describe('utils.test.ts', () => {
   describe('globalId()', () => {
diff --git a/test/utils.ts b/test/utils.ts
index 61f36657..e3ff1044 100644
--- a/test/utils.ts
+++ b/test/utils.ts
@@ -5,12 +5,6 @@ import { join } from 'node:path';
 import { tmpdir, platform } from 'node:os';
 import { randomUUID } from 'node:crypto';
 
-export async function sleep(ms: number) {
-  await new Promise(resolve => {
-    setTimeout(resolve, ms);
-  });
-}
-
 export async function readableToBytes(stream: Readable | ReadableStream) {
   const chunks: Buffer[] = [];
   let chunk: Buffer;
diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json
deleted file mode 100644
index 91644d12..00000000
--- a/tsconfig.eslint.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
-  "extends": "./tsconfig.json",
-  "include": [
-    "src/**/*.ts",
-    "test/**/*.ts"
-  ]
-}
diff --git a/tsconfig.json b/tsconfig.json
index 241f0504..ff41b734 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,35 +1,10 @@
 {
+  "extends": "@eggjs/tsconfig",
   "compilerOptions": {
-    "useUnknownInCatchVariables": true,
-    "allowSyntheticDefaultImports": true,
-    "declaration": true,
-    "downlevelIteration": true,
-    "emitDecoratorMetadata": true,
-    "experimentalDecorators": true,
-    "importHelpers": false,
-    "module": "NodeNext",
-    "moduleResolution": "NodeNext",
-    "newLine": "LF",
-    "checkJs": false,
-    "allowJs": true,
     "strict": true,
-    "skipLibCheck": true,
-    "noImplicitAny": false,
-    "forceConsistentCasingInFileNames": true,
+    "noImplicitAny": true,
     "target": "ES2022",
-    "sourceMap": false,
-    "esModuleInterop": true,
-    "stripInternal": true,
-    "lib": [
-      "ES2022"
-    ],
-    "composite": true,
-    "types": [
-      "node"
-    ],
-    "rootDir": "src"
-  },
-  "include": [
-    "src/**/*.ts"
-  ]
+    "module": "NodeNext",
+    "moduleResolution": "NodeNext"
+  }
 }