nstarter-http-request 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +160 -0
  3. package/dist/cjs/adapter/axios.adapter.js +224 -0
  4. package/dist/cjs/adapter/axios.adapter.js.map +1 -0
  5. package/dist/cjs/adapter/base.js +134 -0
  6. package/dist/cjs/adapter/base.js.map +1 -0
  7. package/dist/cjs/adapter/index.js +20 -0
  8. package/dist/cjs/adapter/index.js.map +1 -0
  9. package/dist/cjs/adapter/undici.adapter.js +272 -0
  10. package/dist/cjs/adapter/undici.adapter.js.map +1 -0
  11. package/dist/cjs/client.js +409 -0
  12. package/dist/cjs/client.js.map +1 -0
  13. package/dist/cjs/config.js +69 -0
  14. package/dist/cjs/config.js.map +1 -0
  15. package/dist/cjs/const/config.const.js +53 -0
  16. package/dist/cjs/const/config.const.js.map +1 -0
  17. package/dist/cjs/const/dns.const.js +16 -0
  18. package/dist/cjs/const/dns.const.js.map +1 -0
  19. package/dist/cjs/const/enum.const.js +22 -0
  20. package/dist/cjs/const/enum.const.js.map +1 -0
  21. package/dist/cjs/const/index.js +21 -0
  22. package/dist/cjs/const/index.js.map +1 -0
  23. package/dist/cjs/const/ip.const.js +110 -0
  24. package/dist/cjs/const/ip.const.js.map +1 -0
  25. package/dist/cjs/index.js +25 -0
  26. package/dist/cjs/index.js.map +1 -0
  27. package/dist/cjs/logger/base.js +60 -0
  28. package/dist/cjs/logger/base.js.map +1 -0
  29. package/dist/cjs/logger/http_client.logger.js +209 -0
  30. package/dist/cjs/logger/http_client.logger.js.map +1 -0
  31. package/dist/cjs/logger/index.js +20 -0
  32. package/dist/cjs/logger/index.js.map +1 -0
  33. package/dist/cjs/logger/log.filter.js +126 -0
  34. package/dist/cjs/logger/log.filter.js.map +1 -0
  35. package/dist/cjs/security/dns.validator.js +137 -0
  36. package/dist/cjs/security/dns.validator.js.map +1 -0
  37. package/dist/cjs/security/index.js +21 -0
  38. package/dist/cjs/security/index.js.map +1 -0
  39. package/dist/cjs/security/ip.validator.js +107 -0
  40. package/dist/cjs/security/ip.validator.js.map +1 -0
  41. package/dist/cjs/security/ssrf.guard.js +180 -0
  42. package/dist/cjs/security/ssrf.guard.js.map +1 -0
  43. package/dist/cjs/security/url.validator.js +170 -0
  44. package/dist/cjs/security/url.validator.js.map +1 -0
  45. package/dist/cjs/tsconfig.tsbuildinfo +1 -0
  46. package/dist/cjs/types/adapter.js +6 -0
  47. package/dist/cjs/types/adapter.js.map +1 -0
  48. package/dist/cjs/types/client.js +6 -0
  49. package/dist/cjs/types/client.js.map +1 -0
  50. package/dist/cjs/types/config.js +6 -0
  51. package/dist/cjs/types/config.js.map +1 -0
  52. package/dist/cjs/types/errors.js +35 -0
  53. package/dist/cjs/types/errors.js.map +1 -0
  54. package/dist/cjs/types/index.js +35 -0
  55. package/dist/cjs/types/index.js.map +1 -0
  56. package/dist/cjs/types/ip.js +6 -0
  57. package/dist/cjs/types/ip.js.map +1 -0
  58. package/dist/cjs/types/logger.js +6 -0
  59. package/dist/cjs/types/logger.js.map +1 -0
  60. package/dist/cjs/types/request_response.js +6 -0
  61. package/dist/cjs/types/request_response.js.map +1 -0
  62. package/dist/cjs/types/security.js +6 -0
  63. package/dist/cjs/types/security.js.map +1 -0
  64. package/dist/cjs/types/trace.js +14 -0
  65. package/dist/cjs/types/trace.js.map +1 -0
  66. package/dist/cjs/utils/common.js +31 -0
  67. package/dist/cjs/utils/common.js.map +1 -0
  68. package/dist/cjs/utils/domain.js +79 -0
  69. package/dist/cjs/utils/domain.js.map +1 -0
  70. package/dist/cjs/utils/index.js +44 -0
  71. package/dist/cjs/utils/index.js.map +1 -0
  72. package/dist/cjs/utils/ip.range.js +200 -0
  73. package/dist/cjs/utils/ip.range.js.map +1 -0
  74. package/dist/cjs/utils/trace.context.js +213 -0
  75. package/dist/cjs/utils/trace.context.js.map +1 -0
  76. package/dist/esm/adapter/axios.adapter.js +184 -0
  77. package/dist/esm/adapter/axios.adapter.js.map +1 -0
  78. package/dist/esm/adapter/base.js +130 -0
  79. package/dist/esm/adapter/base.js.map +1 -0
  80. package/dist/esm/adapter/index.js +4 -0
  81. package/dist/esm/adapter/index.js.map +1 -0
  82. package/dist/esm/adapter/undici.adapter.js +235 -0
  83. package/dist/esm/adapter/undici.adapter.js.map +1 -0
  84. package/dist/esm/client.js +405 -0
  85. package/dist/esm/client.js.map +1 -0
  86. package/dist/esm/config.js +65 -0
  87. package/dist/esm/config.js.map +1 -0
  88. package/dist/esm/const/config.const.js +50 -0
  89. package/dist/esm/const/config.const.js.map +1 -0
  90. package/dist/esm/const/dns.const.js +13 -0
  91. package/dist/esm/const/dns.const.js.map +1 -0
  92. package/dist/esm/const/enum.const.js +19 -0
  93. package/dist/esm/const/enum.const.js.map +1 -0
  94. package/dist/esm/const/index.js +5 -0
  95. package/dist/esm/const/index.js.map +1 -0
  96. package/dist/esm/const/ip.const.js +107 -0
  97. package/dist/esm/const/ip.const.js.map +1 -0
  98. package/dist/esm/index.js +9 -0
  99. package/dist/esm/index.js.map +1 -0
  100. package/dist/esm/logger/base.js +55 -0
  101. package/dist/esm/logger/base.js.map +1 -0
  102. package/dist/esm/logger/http_client.logger.js +205 -0
  103. package/dist/esm/logger/http_client.logger.js.map +1 -0
  104. package/dist/esm/logger/index.js +4 -0
  105. package/dist/esm/logger/index.js.map +1 -0
  106. package/dist/esm/logger/log.filter.js +122 -0
  107. package/dist/esm/logger/log.filter.js.map +1 -0
  108. package/dist/esm/security/dns.validator.js +133 -0
  109. package/dist/esm/security/dns.validator.js.map +1 -0
  110. package/dist/esm/security/index.js +5 -0
  111. package/dist/esm/security/index.js.map +1 -0
  112. package/dist/esm/security/ip.validator.js +103 -0
  113. package/dist/esm/security/ip.validator.js.map +1 -0
  114. package/dist/esm/security/ssrf.guard.js +176 -0
  115. package/dist/esm/security/ssrf.guard.js.map +1 -0
  116. package/dist/esm/security/url.validator.js +166 -0
  117. package/dist/esm/security/url.validator.js.map +1 -0
  118. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  119. package/dist/esm/types/adapter.js +5 -0
  120. package/dist/esm/types/adapter.js.map +1 -0
  121. package/dist/esm/types/client.js +5 -0
  122. package/dist/esm/types/client.js.map +1 -0
  123. package/dist/esm/types/config.js +5 -0
  124. package/dist/esm/types/config.js.map +1 -0
  125. package/dist/esm/types/errors.js +30 -0
  126. package/dist/esm/types/errors.js.map +1 -0
  127. package/dist/esm/types/index.js +19 -0
  128. package/dist/esm/types/index.js.map +1 -0
  129. package/dist/esm/types/ip.js +5 -0
  130. package/dist/esm/types/ip.js.map +1 -0
  131. package/dist/esm/types/logger.js +5 -0
  132. package/dist/esm/types/logger.js.map +1 -0
  133. package/dist/esm/types/request_response.js +5 -0
  134. package/dist/esm/types/request_response.js.map +1 -0
  135. package/dist/esm/types/security.js +5 -0
  136. package/dist/esm/types/security.js.map +1 -0
  137. package/dist/esm/types/trace.js +11 -0
  138. package/dist/esm/types/trace.js.map +1 -0
  139. package/dist/esm/utils/common.js +27 -0
  140. package/dist/esm/utils/common.js.map +1 -0
  141. package/dist/esm/utils/domain.js +71 -0
  142. package/dist/esm/utils/domain.js.map +1 -0
  143. package/dist/esm/utils/index.js +7 -0
  144. package/dist/esm/utils/index.js.map +1 -0
  145. package/dist/esm/utils/ip.range.js +187 -0
  146. package/dist/esm/utils/ip.range.js.map +1 -0
  147. package/dist/esm/utils/trace.context.js +199 -0
  148. package/dist/esm/utils/trace.context.js.map +1 -0
  149. package/dist/types/adapter/axios.adapter.d.ts +51 -0
  150. package/dist/types/adapter/axios.adapter.d.ts.map +1 -0
  151. package/dist/types/adapter/base.d.ts +56 -0
  152. package/dist/types/adapter/base.d.ts.map +1 -0
  153. package/dist/types/adapter/index.d.ts +4 -0
  154. package/dist/types/adapter/index.d.ts.map +1 -0
  155. package/dist/types/adapter/undici.adapter.d.ts +68 -0
  156. package/dist/types/adapter/undici.adapter.d.ts.map +1 -0
  157. package/dist/types/client.d.ts +105 -0
  158. package/dist/types/client.d.ts.map +1 -0
  159. package/dist/types/config.d.ts +14 -0
  160. package/dist/types/config.d.ts.map +1 -0
  161. package/dist/types/const/config.const.d.ts +23 -0
  162. package/dist/types/const/config.const.d.ts.map +1 -0
  163. package/dist/types/const/dns.const.d.ts +13 -0
  164. package/dist/types/const/dns.const.d.ts.map +1 -0
  165. package/dist/types/const/enum.const.d.ts +17 -0
  166. package/dist/types/const/enum.const.d.ts.map +1 -0
  167. package/dist/types/const/index.d.ts +5 -0
  168. package/dist/types/const/index.d.ts.map +1 -0
  169. package/dist/types/const/ip.const.d.ts +42 -0
  170. package/dist/types/const/ip.const.d.ts.map +1 -0
  171. package/dist/types/index.d.ts +9 -0
  172. package/dist/types/index.d.ts.map +1 -0
  173. package/dist/types/logger/base.d.ts +42 -0
  174. package/dist/types/logger/base.d.ts.map +1 -0
  175. package/dist/types/logger/http_client.logger.d.ts +49 -0
  176. package/dist/types/logger/http_client.logger.d.ts.map +1 -0
  177. package/dist/types/logger/index.d.ts +4 -0
  178. package/dist/types/logger/index.d.ts.map +1 -0
  179. package/dist/types/logger/log.filter.d.ts +56 -0
  180. package/dist/types/logger/log.filter.d.ts.map +1 -0
  181. package/dist/types/security/dns.validator.d.ts +61 -0
  182. package/dist/types/security/dns.validator.d.ts.map +1 -0
  183. package/dist/types/security/index.d.ts +5 -0
  184. package/dist/types/security/index.d.ts.map +1 -0
  185. package/dist/types/security/ip.validator.d.ts +31 -0
  186. package/dist/types/security/ip.validator.d.ts.map +1 -0
  187. package/dist/types/security/ssrf.guard.d.ts +54 -0
  188. package/dist/types/security/ssrf.guard.d.ts.map +1 -0
  189. package/dist/types/security/url.validator.d.ts +76 -0
  190. package/dist/types/security/url.validator.d.ts.map +1 -0
  191. package/dist/types/types/adapter.d.ts +30 -0
  192. package/dist/types/types/adapter.d.ts.map +1 -0
  193. package/dist/types/types/client.d.ts +85 -0
  194. package/dist/types/types/client.d.ts.map +1 -0
  195. package/dist/types/types/config.d.ts +99 -0
  196. package/dist/types/types/config.d.ts.map +1 -0
  197. package/dist/types/types/errors.d.ts +23 -0
  198. package/dist/types/types/errors.d.ts.map +1 -0
  199. package/dist/types/types/index.d.ts +10 -0
  200. package/dist/types/types/index.d.ts.map +1 -0
  201. package/dist/types/types/ip.d.ts +32 -0
  202. package/dist/types/types/ip.d.ts.map +1 -0
  203. package/dist/types/types/logger.d.ts +136 -0
  204. package/dist/types/types/logger.d.ts.map +1 -0
  205. package/dist/types/types/request_response.d.ts +54 -0
  206. package/dist/types/types/request_response.d.ts.map +1 -0
  207. package/dist/types/types/security.d.ts +115 -0
  208. package/dist/types/types/security.d.ts.map +1 -0
  209. package/dist/types/types/trace.d.ts +34 -0
  210. package/dist/types/types/trace.d.ts.map +1 -0
  211. package/dist/types/utils/common.d.ts +14 -0
  212. package/dist/types/utils/common.d.ts.map +1 -0
  213. package/dist/types/utils/domain.d.ts +39 -0
  214. package/dist/types/utils/domain.d.ts.map +1 -0
  215. package/dist/types/utils/index.d.ts +6 -0
  216. package/dist/types/utils/index.d.ts.map +1 -0
  217. package/dist/types/utils/ip.range.d.ts +61 -0
  218. package/dist/types/utils/ip.range.d.ts.map +1 -0
  219. package/dist/types/utils/trace.context.d.ts +106 -0
  220. package/dist/types/utils/trace.context.d.ts.map +1 -0
  221. package/docs/adapters.md +53 -0
  222. package/docs/configuration.md +149 -0
  223. package/docs/logging.md +70 -0
  224. package/docs/proxy.md +44 -0
  225. package/docs/security.md +56 -0
  226. package/docs/trace-context.md +436 -0
  227. package/package.json +50 -0
  228. package/src/adapter/axios.adapter.ts +228 -0
  229. package/src/adapter/base.ts +180 -0
  230. package/src/adapter/index.ts +3 -0
  231. package/src/adapter/undici.adapter.ts +282 -0
  232. package/src/client.ts +552 -0
  233. package/src/config.ts +86 -0
  234. package/src/const/config.const.ts +60 -0
  235. package/src/const/dns.const.ts +15 -0
  236. package/src/const/enum.const.ts +17 -0
  237. package/src/const/index.ts +4 -0
  238. package/src/const/ip.const.ts +139 -0
  239. package/src/index.ts +8 -0
  240. package/src/logger/base.ts +75 -0
  241. package/src/logger/http_client.logger.ts +272 -0
  242. package/src/logger/index.ts +3 -0
  243. package/src/logger/log.filter.ts +149 -0
  244. package/src/security/dns.validator.ts +170 -0
  245. package/src/security/index.ts +4 -0
  246. package/src/security/ip.validator.ts +124 -0
  247. package/src/security/ssrf.guard.ts +224 -0
  248. package/src/security/url.validator.ts +192 -0
  249. package/src/types/adapter.ts +38 -0
  250. package/src/types/client.ts +119 -0
  251. package/src/types/config.ts +110 -0
  252. package/src/types/errors.ts +38 -0
  253. package/src/types/index.ts +27 -0
  254. package/src/types/ip.ts +34 -0
  255. package/src/types/logger.ts +150 -0
  256. package/src/types/request_response.ts +65 -0
  257. package/src/types/security.ts +126 -0
  258. package/src/types/trace.ts +35 -0
  259. package/src/utils/common.ts +28 -0
  260. package/src/utils/domain.ts +78 -0
  261. package/src/utils/index.ts +7 -0
  262. package/src/utils/ip.range.ts +218 -0
  263. package/src/utils/trace.context.ts +240 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Axios 适配器实现
3
+ */
4
+
5
+ import type {
6
+ HttpRequestOptions,
7
+ HttpResponse,
8
+ ProxyConfig
9
+ } from '../types';
10
+ import { HttpError } from '../types';
11
+ import { BaseHttpAdapter } from './base';
12
+ import type {
13
+ AxiosProxyConfig,
14
+ AxiosRequestConfig
15
+ } from 'axios';
16
+ import axios from 'axios';
17
+ import { HttpsProxyAgent } from 'https-proxy-agent';
18
+ import * as https from 'https';
19
+ import { EClientAdapter } from '../const';
20
+
21
+
22
+ /**
23
+ * Axios 适配器
24
+ */
25
+ export class AxiosAdapter extends BaseHttpAdapter {
26
+ public readonly name = EClientAdapter.axios as const;
27
+
28
+ /**
29
+ * 检查 Axios 是否可用
30
+ */
31
+ public isAvailable(): boolean {
32
+ return !!axios;
33
+ }
34
+
35
+ /**
36
+ * 发送请求
37
+ * @param options 请求选项
38
+ */
39
+ protected async sendRequest<T>(options: HttpRequestOptions): Promise<HttpResponse<T>> {
40
+ if (!this.isAvailable()) {
41
+ throw new Error('Axios is not available. Please install it: npm install axios');
42
+ }
43
+
44
+ const url = this.buildUrl(options.url, options.params);
45
+ const axiosConfig = this.buildAxiosConfig(options, url);
46
+
47
+ try {
48
+ const startTime = Date.now();
49
+ const axiosResponse = await axios(axiosConfig);
50
+ const duration = Date.now() - startTime;
51
+
52
+ this.logger?.debug('HTTP request completed', {
53
+ method: options.method,
54
+ url,
55
+ status: axiosResponse.status,
56
+ duration
57
+ });
58
+
59
+ // 转换为标准响应格式
60
+ const response: HttpResponse<T> = {
61
+ status: axiosResponse.status,
62
+ statusText: axiosResponse.statusText,
63
+ headers: this.normalizeHeaders(axiosResponse.headers),
64
+ data: axiosResponse.data,
65
+ config: options
66
+ };
67
+
68
+ return response;
69
+ } catch (error: any) {
70
+ throw this.handleAxiosError(error, options);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 构建 Axios 配置
76
+ * @param options 请求选项
77
+ * @param url 完整 URL
78
+ */
79
+ private buildAxiosConfig(options: HttpRequestOptions, url: string): AxiosRequestConfig {
80
+ const axiosConfig: AxiosRequestConfig = {
81
+ method: options.method.toLowerCase(),
82
+ url,
83
+ headers: options.headers || {},
84
+ timeout: options.timeout ?? this.requestConfig.timeout,
85
+ maxRedirects: this.requestConfig.maxRedirects,
86
+ maxContentLength: this.requestConfig.maxResponseBodySize,
87
+ maxBodyLength: this.requestConfig.maxRequestBodySize,
88
+ validateStatus: () => true,
89
+ httpsAgent: new https.Agent({
90
+ rejectUnauthorized: this.securityConfig.rejectUnauthorized
91
+ })
92
+ };
93
+
94
+ // 设置请求体
95
+ if (options.body !== undefined) {
96
+ axiosConfig.data = options.body;
97
+ }
98
+
99
+ // 设置响应类型
100
+ if (options.responseType) {
101
+ axiosConfig.responseType = options.responseType;
102
+ }
103
+
104
+ // 配置代理
105
+ this.configureAxiosProxy(axiosConfig, options);
106
+
107
+ return axiosConfig;
108
+ }
109
+
110
+ /**
111
+ * 配置 Axios 代理
112
+ * @param axiosConfig Axios 配置对象
113
+ * @param options 请求选项
114
+ */
115
+ private configureAxiosProxy(axiosConfig: AxiosRequestConfig, options: HttpRequestOptions): void {
116
+ // 设置代理(全局配置)
117
+ if (this.requestConfig.proxy) {
118
+ axiosConfig.proxy = this.buildProxyConfig(this.requestConfig.proxy);
119
+ } else {
120
+ // 显式的禁用代理, 防止axios默认从环境变量中 HTTP_PROXY、HTTPS_PROXY 读取代理配置.
121
+ axiosConfig.proxy = false;
122
+ }
123
+
124
+ // 设置正向代理(如果请求元数据中包含代理信息)
125
+ const forwardProxyMeta = options.meta?.__forwardProxy;
126
+ if (forwardProxyMeta) {
127
+ this.applyForwardProxy(axiosConfig, forwardProxyMeta);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * 应用正向代理配置
133
+ * @param axiosConfig Axios 配置对象
134
+ * @param forwardProxyMeta 正向代理元数据
135
+ */
136
+ private applyForwardProxy(
137
+ axiosConfig: AxiosRequestConfig,
138
+ forwardProxyMeta: { proxyUrl: string, isHttps: boolean }
139
+ ): void {
140
+ const { proxyUrl, isHttps } = forwardProxyMeta;
141
+
142
+ if (isHttps) {
143
+ // HTTPS 请求使用 HttpsProxyAgent,并应用证书校验配置
144
+ axiosConfig.httpsAgent = new HttpsProxyAgent(proxyUrl as `https://${ string }`, {
145
+ rejectUnauthorized: this.securityConfig.rejectUnauthorized
146
+ });
147
+ axiosConfig.proxy = false;
148
+ this.logger?.debug('Using HttpsProxyAgent for HTTPS request', {
149
+ proxyUrl,
150
+ rejectUnauthorized: this.securityConfig.rejectUnauthorized
151
+ });
152
+ } else {
153
+ // HTTP 请求直接设置 proxy
154
+ const parsedProxyUrl = new URL(proxyUrl);
155
+ axiosConfig.proxy = {
156
+ host: parsedProxyUrl.hostname,
157
+ port: Number(parsedProxyUrl.port),
158
+ protocol: 'http'
159
+ };
160
+
161
+ // 处理代理认证
162
+ if (parsedProxyUrl.username && parsedProxyUrl.password) {
163
+ axiosConfig.proxy.auth = {
164
+ username: decodeURIComponent(parsedProxyUrl.username),
165
+ password: decodeURIComponent(parsedProxyUrl.password)
166
+ };
167
+ }
168
+
169
+ this.logger?.debug('Using proxy for HTTP request', {
170
+ host: axiosConfig.proxy.host,
171
+ port: axiosConfig.proxy.port
172
+ });
173
+ }
174
+ }
175
+
176
+ /**
177
+ * 处理 Axios 错误
178
+ * @param error 错误对象
179
+ * @param options 请求选项
180
+ */
181
+ private handleAxiosError(error: any, options: HttpRequestOptions): HttpError {
182
+ if (error.isAxiosError) {
183
+ return new HttpError(
184
+ error.message,
185
+ error.code || 'AXIOS_ERROR',
186
+ error.response?.status,
187
+ error.response ? {
188
+ status: error.response.status,
189
+ statusText: error.response.statusText,
190
+ headers: this.normalizeHeaders(error.response.headers),
191
+ data: error.response.data,
192
+ config: options
193
+ } : undefined,
194
+ options
195
+ );
196
+ }
197
+
198
+ return new HttpError(
199
+ error.message || 'Unknown error',
200
+ 'UNKNOWN_ERROR',
201
+ undefined,
202
+ undefined,
203
+ options
204
+ );
205
+ }
206
+
207
+ /**
208
+ * 构建代理配置
209
+ * @param proxy 代理配置
210
+ */
211
+ private buildProxyConfig(proxy: ProxyConfig): AxiosProxyConfig {
212
+ const config: AxiosProxyConfig = {
213
+ host: proxy.host,
214
+ port: proxy.port,
215
+ protocol: proxy.protocol
216
+ };
217
+
218
+ if (proxy.auth) {
219
+ config.auth = {
220
+ username: proxy.auth.username,
221
+ password: proxy.auth.password
222
+ };
223
+ }
224
+
225
+ return config;
226
+ }
227
+ }
228
+
@@ -0,0 +1,180 @@
1
+ /**
2
+ * HTTP 适配器基础实现
3
+ */
4
+
5
+ import type {
6
+ IHttpAdapter,
7
+ HttpRequestOptions,
8
+ HttpResponse,
9
+ RequestConfig,
10
+ SecurityConfig,
11
+ ILogger
12
+ } from '../types';
13
+ import { CommonUtils } from '../utils';
14
+ import type { EClientAdapter } from '../const';
15
+
16
+ /**
17
+ * HTTP 适配器抽象基类
18
+ */
19
+ export abstract class BaseHttpAdapter implements IHttpAdapter {
20
+ protected requestConfig: RequestConfig;
21
+ protected securityConfig: SecurityConfig;
22
+ protected logger?: ILogger;
23
+
24
+ constructor(requestConfig: RequestConfig, securityConfig: SecurityConfig, logger?: ILogger) {
25
+ this.requestConfig = requestConfig;
26
+ this.securityConfig = securityConfig;
27
+ this.logger = logger;
28
+ }
29
+
30
+ public abstract readonly name: EClientAdapter;
31
+
32
+ /**
33
+ * 发送 HTTP 请求(由子类实现)
34
+ * @param options 请求选项
35
+ */
36
+ protected abstract sendRequest<T>(options: HttpRequestOptions): Promise<HttpResponse<T>>;
37
+
38
+ /**
39
+ * 检查适配器是否可用
40
+ */
41
+ public abstract isAvailable(): boolean;
42
+
43
+ /**
44
+ * 发送请求(带重试机制)
45
+ * @param options 请求选项
46
+ */
47
+ public async request<T = any>(options: HttpRequestOptions): Promise<HttpResponse<T>> {
48
+ const retries = options.retries ?? this.requestConfig.retries;
49
+ const retryDelay = this.requestConfig.retryDelay;
50
+ const timeout = options.timeout ?? this.requestConfig.timeout;
51
+
52
+ // 合并选项
53
+ const finalOptions: HttpRequestOptions = {
54
+ ...options,
55
+ timeout
56
+ };
57
+
58
+ let lastError: any;
59
+
60
+ for (let attempt = 0; attempt <= retries; attempt++) {
61
+ try {
62
+ this.logger?.debug(`HTTP request attempt ${ attempt + 1 }/${ retries + 1 }`, {
63
+ method: options.method,
64
+ url: options.url,
65
+ scenario: options.scenario
66
+ });
67
+
68
+ const response = await this.sendRequest<T>(finalOptions);
69
+
70
+ if (attempt > 0) {
71
+ this.logger?.debug(`HTTP request succeeded after ${ attempt } retries`, {
72
+ method: options.method,
73
+ url: options.url,
74
+ status: response.status
75
+ });
76
+ }
77
+
78
+ return response;
79
+ } catch (error) {
80
+ lastError = error;
81
+
82
+ this.logger?.debug(`HTTP request attempt ${ attempt + 1 } failed`, {
83
+ method: options.method,
84
+ url: options.url,
85
+ error: error instanceof Error ? error.message : String(error)
86
+ });
87
+
88
+ // 最后一次尝试,不再重试
89
+ if (attempt === retries) {
90
+ break;
91
+ }
92
+
93
+ // 等待后重试
94
+ await CommonUtils.delay(retryDelay * (attempt + 1));
95
+ }
96
+ }
97
+
98
+ // 所有尝试都失败了
99
+ this.logger?.debug('All HTTP request attempts failed', {
100
+ method: options.method,
101
+ url: options.url,
102
+ attempts: retries + 1,
103
+ error: lastError instanceof Error ? lastError.message : String(lastError)
104
+ });
105
+
106
+ throw lastError;
107
+ }
108
+
109
+ /**
110
+ * 更新请求配置
111
+ * @param config 新的请求配置
112
+ */
113
+ public updateConfig(config: Partial<RequestConfig>): void {
114
+ this.requestConfig = { ...this.requestConfig, ...config };
115
+ }
116
+
117
+ /**
118
+ * 设置日志记录器
119
+ * @param logger 日志记录器
120
+ */
121
+ public setLogger(logger: ILogger): void {
122
+ this.logger = logger;
123
+ }
124
+
125
+ /**
126
+ * 构建查询字符串
127
+ * @param params 查询参数
128
+ */
129
+ protected buildQueryString(params?: Record<string, string | number | boolean>): string {
130
+ if (!params || Object.keys(params).length === 0) {
131
+ return '';
132
+ }
133
+
134
+ const searchParams = new URLSearchParams();
135
+ for (const [key, value] of Object.entries(params)) {
136
+ searchParams.append(key, String(value));
137
+ }
138
+
139
+ return searchParams.toString();
140
+ }
141
+
142
+ /**
143
+ * 构建完整 URL
144
+ * @param url 基础 URL
145
+ * @param params 查询参数
146
+ */
147
+ protected buildUrl(url: string, params?: Record<string, string | number | boolean>): string {
148
+ const queryString = this.buildQueryString(params);
149
+ if (!queryString) {
150
+ return url;
151
+ }
152
+
153
+ const separator = url.includes('?') ? '&' : '?';
154
+ return `${ url }${ separator }${ queryString }`;
155
+ }
156
+
157
+ /**
158
+ * 标准化响应头
159
+ * @param headers 原始响应头
160
+ */
161
+ protected normalizeHeaders(headers: any): Record<string, string> {
162
+ const normalized: Record<string, string> = {};
163
+
164
+ if (!headers) {
165
+ return normalized;
166
+ }
167
+
168
+ // 处理不同的头格式
169
+ if (typeof headers === 'object') {
170
+ for (const [key, value] of Object.entries(headers)) {
171
+ if (value !== undefined && value !== null) {
172
+ normalized[key.toLowerCase()] = String(value);
173
+ }
174
+ }
175
+ }
176
+
177
+ return normalized;
178
+ }
179
+ }
180
+
@@ -0,0 +1,3 @@
1
+ export * from './base';
2
+ export * from './axios.adapter';
3
+ export * from './undici.adapter';
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Undici 适配器实现
3
+ */
4
+
5
+ import type {
6
+ HttpRequestOptions,
7
+ HttpResponse,
8
+ ResponseType,
9
+ } from '../types';
10
+ import { HttpError } from '../types';
11
+ import { BaseHttpAdapter } from './base';
12
+ import undici, {
13
+ Agent,
14
+ ProxyAgent,
15
+ type Dispatcher
16
+ } from 'undici';
17
+ import { STATUS_CODES } from 'http';
18
+ import { EClientAdapter } from '../const';
19
+
20
+ /**
21
+ * Undici 适配器
22
+ */
23
+ export class UndiciAdapter extends BaseHttpAdapter {
24
+ public readonly name = EClientAdapter.undici as const;
25
+
26
+ /**
27
+ * 检查 Undici 是否可用
28
+ */
29
+ public isAvailable(): boolean {
30
+ return !!undici;
31
+ }
32
+
33
+ /**
34
+ * 发送请求
35
+ * @param options 请求选项
36
+ */
37
+ protected async sendRequest<T>(options: HttpRequestOptions): Promise<HttpResponse<T>> {
38
+ if (!this.isAvailable()) {
39
+ throw new Error('Undici is not available. Please install it: npm install undici');
40
+ }
41
+
42
+ const fullUrl = this.buildUrl(options.url, options.params);
43
+ const headers = this.prepareHeaders(options.headers);
44
+ const body = this.prepareBody(options.body, headers);
45
+ const dispatcher = this.buildDispatcher(options);
46
+ const undiciOptions = this.buildUndiciOptions(options, headers, body, dispatcher);
47
+
48
+ try {
49
+ const startTime = Date.now();
50
+ const response = await undici.request(fullUrl, undiciOptions);
51
+ const duration = Date.now() - startTime;
52
+
53
+ this.logger?.debug('HTTP request completed', {
54
+ method: options.method,
55
+ url: fullUrl,
56
+ status: response.statusCode,
57
+ duration
58
+ });
59
+
60
+ const data = await this.readResponseBody<T>(response, options.responseType);
61
+
62
+ const httpResponse: HttpResponse<T> = {
63
+ status: response.statusCode,
64
+ statusText: this.getStatusText(response.statusCode),
65
+ headers: this.normalizeHeaders(response.headers),
66
+ data,
67
+ config: options
68
+ };
69
+
70
+ return httpResponse;
71
+ } catch (error: unknown) {
72
+ throw this.handleUndiciError(error, options);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 准备请求头
78
+ * @param headers 原始请求头
79
+ */
80
+ private prepareHeaders(headers?: Record<string, string | number | string[]>): Record<string, string | string[]> {
81
+ const prepared: Record<string, string | string[]> = {};
82
+ if (headers) {
83
+ for (const [key, value] of Object.entries(headers)) {
84
+ prepared[key] = typeof value === 'number' ? String(value) : value;
85
+ }
86
+ }
87
+ return prepared;
88
+ }
89
+
90
+ /**
91
+ * 准备请求体
92
+ * @param body 原始请求体
93
+ * @param headers 请求头
94
+ */
95
+ private prepareBody(body: any, headers: Record<string, string | string[]>): Dispatcher.DispatchOptions['body'] {
96
+ if (body === undefined) {
97
+ return undefined;
98
+ }
99
+
100
+ if (typeof body === 'object' && !(body instanceof Buffer)) {
101
+ // 自动设置 Content-Type
102
+ if (!headers['content-type'] && !headers['Content-Type']) {
103
+ headers['Content-Type'] = 'application/json';
104
+ }
105
+ return JSON.stringify(body);
106
+ }
107
+
108
+ return body;
109
+ }
110
+
111
+ /**
112
+ * 构建 Dispatcher(代理或 TLS 配置)
113
+ * @param options 请求选项
114
+ */
115
+ private buildDispatcher(options: HttpRequestOptions): Dispatcher | undefined {
116
+ let dispatcher: Dispatcher | undefined;
117
+
118
+ // 如果 rejectUnauthorized 为 false,需要创建 Agent 来设置 TLS 选项
119
+ if (!this.securityConfig.rejectUnauthorized) {
120
+ dispatcher = new Agent({
121
+ connect: {
122
+ rejectUnauthorized: false
123
+ }
124
+ });
125
+ }
126
+
127
+ // 有代理时,创建 ProxyAgent
128
+ if (this.requestConfig.proxy) {
129
+ dispatcher = this.createGlobalProxyAgent();
130
+ }
131
+
132
+ // 设置正向代理(如果请求元数据中包含代理信息,会覆盖全局代理配置)
133
+ const forwardProxyMeta = options.meta?.__forwardProxy;
134
+ if (forwardProxyMeta) {
135
+ dispatcher = this.createForwardProxyAgent(forwardProxyMeta.proxyUrl);
136
+ }
137
+
138
+ return dispatcher;
139
+ }
140
+
141
+ /**
142
+ * 创建全局代理 Agent
143
+ */
144
+ private createGlobalProxyAgent(): ProxyAgent {
145
+ const { protocol, host, port, auth } = this.requestConfig.proxy!;
146
+ let proxyUrl = `${ protocol }://${ host }:${ port }`;
147
+
148
+ if (auth) {
149
+ const encodedUsername = encodeURIComponent(auth.username);
150
+ const encodedPassword = encodeURIComponent(auth.password);
151
+ proxyUrl = `${ protocol }://${ encodedUsername }:${ encodedPassword }@${ host }:${ port }`;
152
+ }
153
+
154
+ const agent = new ProxyAgent({
155
+ uri: proxyUrl,
156
+ connect: {
157
+ rejectUnauthorized: this.securityConfig.rejectUnauthorized
158
+ }
159
+ });
160
+
161
+ this.logger?.debug('Using ProxyAgent with global proxy config', {
162
+ host,
163
+ port,
164
+ protocol,
165
+ rejectUnauthorized: this.securityConfig.rejectUnauthorized
166
+ });
167
+
168
+ return agent;
169
+ }
170
+
171
+ /**
172
+ * 创建正向代理 Agent
173
+ * @param proxyUrl 代理 URL
174
+ */
175
+ private createForwardProxyAgent(proxyUrl: string): ProxyAgent {
176
+ const agent = new ProxyAgent({
177
+ uri: proxyUrl,
178
+ connect: {
179
+ rejectUnauthorized: this.securityConfig.rejectUnauthorized
180
+ }
181
+ });
182
+
183
+ this.logger?.debug('Using ProxyAgent for forward proxy (overrides global proxy)', {
184
+ proxyUrl,
185
+ rejectUnauthorized: this.securityConfig.rejectUnauthorized
186
+ });
187
+
188
+ return agent;
189
+ }
190
+
191
+ /**
192
+ * 构建 Undici 请求选项
193
+ */
194
+ private buildUndiciOptions(
195
+ options: HttpRequestOptions,
196
+ headers: Record<string, string | string[]>,
197
+ body: Dispatcher.DispatchOptions['body'],
198
+ dispatcher?: Dispatcher
199
+ ): Parameters<typeof undici.request>[1] {
200
+ return {
201
+ method: options.method as Dispatcher.HttpMethod,
202
+ headers,
203
+ body,
204
+ bodyTimeout: options.timeout ?? this.requestConfig.timeout,
205
+ headersTimeout: options.timeout ?? this.requestConfig.timeout,
206
+ maxRedirections: this.requestConfig.maxRedirects,
207
+ ...(dispatcher && { dispatcher }),
208
+ };
209
+ }
210
+
211
+ /**
212
+ * 处理 Undici 错误
213
+ * @param error 错误对象
214
+ * @param options 请求选项
215
+ */
216
+ private handleUndiciError(error: unknown, options: HttpRequestOptions): HttpError {
217
+ const errorMessage = error instanceof Error ? error.message : 'Undici request failed';
218
+ const errorCode = (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string')
219
+ ? error.code
220
+ : 'UNDICI_ERROR';
221
+
222
+ return new HttpError(
223
+ errorMessage,
224
+ errorCode,
225
+ undefined,
226
+ undefined,
227
+ options
228
+ );
229
+ }
230
+
231
+ /**
232
+ * 读取响应体
233
+ * @param response Undici 响应对象
234
+ * @param responseType 响应类型
235
+ */
236
+ private async readResponseBody<T>(response: Dispatcher.ResponseData, responseType?: ResponseType): Promise<T> {
237
+ try {
238
+ switch (responseType) {
239
+ case 'text': {
240
+ return (await response.body.text()) as T;
241
+ }
242
+ case 'arraybuffer': {
243
+ return (await response.body.arrayBuffer()) as T;
244
+ }
245
+ case 'stream': {
246
+ // undici 返回 BodyReadable (继承自 Node.js Readable)
247
+ return response.body as T;
248
+ }
249
+ case 'blob': {
250
+ return (await response.body.blob()) as T;
251
+ }
252
+ case 'formdata': {
253
+ return (await response.body.formData()) as T;
254
+ }
255
+ case 'json': {
256
+ return (await response.body.json()) as T;
257
+ }
258
+ default: {
259
+ const text = await response.body.text();
260
+ try {
261
+ return JSON.parse(text) as T;
262
+ } catch {
263
+ // 如果不是有效的 JSON,返回原文本
264
+ return text as T;
265
+ }
266
+ }
267
+
268
+ }
269
+ } catch (error) {
270
+ throw new Error(`Failed to read response body: ${ error instanceof Error ? error.message : String(error) }`);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * 获取状态文本
276
+ * @param statusCode 状态码
277
+ */
278
+ private getStatusText(statusCode: number): string {
279
+ return STATUS_CODES[statusCode] || 'Unknown';
280
+ }
281
+ }
282
+