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
package/src/client.ts ADDED
@@ -0,0 +1,552 @@
1
+ /**
2
+ * HTTP 客户端核心类
3
+ */
4
+
5
+ import type {
6
+ HttpClientConfig,
7
+ PartialHttpClientConfig,
8
+ HttpRequestOptions,
9
+ HttpResponse,
10
+ IHttpAdapter,
11
+ IHttpClient,
12
+ ILogger,
13
+ IHttpClientLogger,
14
+ IForwardProxy,
15
+ ISsrfGuard,
16
+ IStreamMetrics
17
+ } from './types';
18
+ import {
19
+ mergeConfig,
20
+ validateConfig
21
+ } from './config';
22
+ import { SsrfGuard } from './security';
23
+ import {
24
+ AxiosAdapter,
25
+ UndiciAdapter
26
+ } from './adapter';
27
+ import {
28
+ NullLogger,
29
+ NsHttpClientLogger
30
+ } from './logger';
31
+ import { EClientAdapter } from './const';
32
+ import { HttpError } from './types';
33
+ import {
34
+ DomainUtils,
35
+ TraceContextUtils
36
+ } from './utils';
37
+ import {
38
+ PassThrough,
39
+ Readable
40
+ } from 'node:stream';
41
+
42
+ /**
43
+ * HTTP 客户端
44
+ */
45
+ export class NsHttpClient implements IHttpClient {
46
+ private config: HttpClientConfig;
47
+ private adapter: IHttpAdapter;
48
+ private ssrfGuard: ISsrfGuard;
49
+ private logger: ILogger;
50
+ private httpLogger?: IHttpClientLogger;
51
+ private forwardProxy?: IForwardProxy;
52
+
53
+ constructor(userConfig: PartialHttpClientConfig = {}) {
54
+ // 合并配置
55
+ this.config = mergeConfig(userConfig);
56
+
57
+ // 验证配置
58
+ validateConfig(this.config);
59
+
60
+ // 初始化日志记录器
61
+ this.logger = this.initLogger();
62
+ this.httpLogger = this.initHttpLogger();
63
+
64
+ // 初始化 SSRF 防护
65
+ this.ssrfGuard = new SsrfGuard(this.config.security);
66
+
67
+ // 初始化适配器
68
+ this.adapter = this.initAdapter();
69
+
70
+ this.logger.debug('NsHttpClient initialized', {
71
+ adapter: this.config.adapter,
72
+ security: {
73
+ enableDnsValidation: this.config.security.enableDnsValidation,
74
+ enableIpValidation: this.config.security.enableIpValidation,
75
+ allowPrivateIp: this.config.security.allowPrivateIp
76
+ }
77
+ });
78
+ }
79
+
80
+ /**
81
+ * 从配置创建 HTTP 客户端
82
+ * @param config
83
+ */
84
+ public static fromHttpClientConfig(config: PartialHttpClientConfig = {}): NsHttpClient {
85
+ return new NsHttpClient(config);
86
+ }
87
+
88
+ /**
89
+ * 挂载适配器.
90
+ * @param adapter
91
+ */
92
+ public withAdapter(adapter: EClientAdapter): this {
93
+ this.config.adapter = adapter;
94
+ this.adapter = this.initAdapter();
95
+ return this;
96
+ }
97
+
98
+ /**
99
+ * 挂载正向代理.
100
+ * @param proxy
101
+ */
102
+ public withForwardProxy(proxy: IForwardProxy): this {
103
+ this.forwardProxy = proxy;
104
+ this.logger.info('Forward proxy configured', {
105
+ enabled: proxy.enabled,
106
+ http_proxy: proxy.http_proxy,
107
+ https_proxy: proxy.https_proxy,
108
+ no_proxy_count: proxy.no_proxy?.length || 0
109
+ });
110
+ return this;
111
+ }
112
+
113
+ /**
114
+ * 初始化日志记录器
115
+ */
116
+ private initLogger(): ILogger {
117
+ if (!this.config.logging.enabled) {
118
+ return new NullLogger();
119
+ }
120
+
121
+ return this.config.logging.logger || new NullLogger();
122
+ }
123
+
124
+ /**
125
+ * 初始化 HTTP 客户端日志记录器
126
+ */
127
+ private initHttpLogger(): IHttpClientLogger | undefined {
128
+ if (!this.config.logging.enabled) {
129
+ return undefined;
130
+ }
131
+
132
+ // 如果用户提供了自定义的 httpLogger,直接使用
133
+ if (this.config.logging.httpLogger) {
134
+ return this.config.logging.httpLogger;
135
+ }
136
+
137
+ // 否则,使用 NsHttpClientLogger 包装通用的 logger
138
+ return NsHttpClientLogger.create(
139
+ this.logger,
140
+ this.config.logging.filter,
141
+ {
142
+ logRequestBody: this.config.logging.logRequestBody,
143
+ logResponseBody: this.config.logging.logResponseBody,
144
+ bodyMaxLength: this.config.logging.bodyMaxLength
145
+ }
146
+ );
147
+ }
148
+
149
+ /**
150
+ * 初始化适配器
151
+ */
152
+ private initAdapter(): IHttpAdapter {
153
+ const adapter = this.config.adapter === EClientAdapter.axios
154
+ ? new AxiosAdapter(this.config.request, this.config.security, this.logger)
155
+ : new UndiciAdapter(this.config.request, this.config.security, this.logger);
156
+
157
+ if (!adapter.isAvailable()) {
158
+ throw new Error(
159
+ `Adapter "${ this.config.adapter }" is not available. ` +
160
+ `Please install it: npm install ${ this.config.adapter }`
161
+ );
162
+ }
163
+
164
+ return adapter;
165
+ }
166
+
167
+ /**
168
+ * 发送 HTTP 请求
169
+ * @param options 请求选项
170
+ * @returns 响应结果
171
+ */
172
+ public async request<T = any>(options: HttpRequestOptions): Promise<HttpResponse<T>> {
173
+ const startTime = Date.now();
174
+
175
+ try {
176
+ // 安全验证
177
+ const { safeUrl, result: ssrfResult } = await this.ssrfGuard.validateAndGetSafeUrl(options.url);
178
+
179
+ this.logger.debug('URL validation passed', {
180
+ originalUrl: options.url,
181
+ safeUrl,
182
+ resolvedIps: ssrfResult.resolvedIps,
183
+ scenario: options.scenario
184
+ });
185
+
186
+ // 使用安全的 URL 发起请求
187
+ const safeOptions: HttpRequestOptions = {
188
+ ...options,
189
+ url: safeUrl
190
+ };
191
+
192
+ // 如果 URL 被替换为 IP,需要设置 Host 头
193
+ if (ssrfResult.safeIp && ssrfResult.hostname !== ssrfResult.safeIp) {
194
+ safeOptions.headers = {
195
+ ...safeOptions.headers,
196
+ Host: ssrfResult.hostname
197
+ };
198
+ }
199
+
200
+ // 处理 W3C Trace Context traceparent 头
201
+ if (options.traceparent) {
202
+ // 验证 traceparent 格式
203
+ if (!TraceContextUtils.isValidTraceParent(options.traceparent)) {
204
+ this.logger.warn('Invalid traceparent format, ignoring', {
205
+ traceparent: options.traceparent
206
+ });
207
+ } else {
208
+ safeOptions.headers = {
209
+ ...safeOptions.headers,
210
+ traceparent: options.traceparent
211
+ };
212
+
213
+ this.logger.debug('Traceparent header added', {
214
+ traceparent: options.traceparent,
215
+ sampled: TraceContextUtils.isSampled(options.traceparent)
216
+ });
217
+ }
218
+ }
219
+
220
+ // 应用正向代理配置
221
+ if (this.forwardProxy) {
222
+ this.applyForwardProxy(safeOptions);
223
+ }
224
+
225
+ // 发起请求
226
+ const response = await this.adapter.request<T>(safeOptions);
227
+
228
+ // 首次响应时间(TTFB)
229
+ const ttfb = Date.now() - startTime;
230
+
231
+ // 如果是流式响应,包装流对象以追踪完整生命周期
232
+ if (options.responseType === 'stream' && this.isStreamResponse(response.data)) {
233
+ return this.wrapStreamResponse(safeOptions, response, startTime, ttfb);
234
+ }
235
+
236
+ // 记录非流式响应日志
237
+ this.logResponse(safeOptions, response, ttfb);
238
+
239
+ return response;
240
+ } catch (error) {
241
+ // 记录错误日志
242
+ this.logError(options, error, Date.now() - startTime);
243
+ throw error;
244
+ }
245
+ }
246
+
247
+ /**
248
+ * 便捷方法:GET 请求
249
+ */
250
+ public async get<T = any>(
251
+ url: string,
252
+ options?: Partial<Omit<HttpRequestOptions, 'method' | 'url'>>
253
+ ): Promise<HttpResponse<T>> {
254
+ return this.request<T>({ ...options, method: 'GET', url });
255
+ }
256
+
257
+ /**
258
+ * 便捷方法:POST 请求
259
+ */
260
+ public async post<T = any>(
261
+ url: string,
262
+ body?: any,
263
+ options?: Partial<Omit<HttpRequestOptions, 'method' | 'url' | 'body'>>
264
+ ): Promise<HttpResponse<T>> {
265
+ return this.request<T>({ ...options, method: 'POST', url, body });
266
+ }
267
+
268
+ /**
269
+ * 便捷方法:PUT 请求
270
+ */
271
+ public async put<T = any>(
272
+ url: string,
273
+ body?: any,
274
+ options?: Partial<Omit<HttpRequestOptions, 'method' | 'url' | 'body'>>
275
+ ): Promise<HttpResponse<T>> {
276
+ return this.request<T>({ ...options, method: 'PUT', url, body });
277
+ }
278
+
279
+ /**
280
+ * 便捷方法:DELETE 请求
281
+ */
282
+ public async delete<T = any>(
283
+ url: string,
284
+ options?: Partial<Omit<HttpRequestOptions, 'method' | 'url'>>
285
+ ): Promise<HttpResponse<T>> {
286
+ return this.request<T>({ ...options, method: 'DELETE', url });
287
+ }
288
+
289
+ /**
290
+ * 便捷方法:PATCH 请求
291
+ */
292
+ public async patch<T = any>(
293
+ url: string,
294
+ body?: any,
295
+ options?: Partial<Omit<HttpRequestOptions, 'method' | 'url' | 'body'>>
296
+ ): Promise<HttpResponse<T>> {
297
+ return this.request<T>({ ...options, method: 'PATCH', url, body });
298
+ }
299
+
300
+ /**
301
+ * 记录响应日志
302
+ */
303
+ private logResponse(
304
+ options: HttpRequestOptions,
305
+ response: HttpResponse,
306
+ duration: number,
307
+ streamMetrics?: IStreamMetrics
308
+ ): void {
309
+ if (!this.httpLogger) {
310
+ return;
311
+ }
312
+
313
+ // 计算实际重试次数
314
+ const retryCount = options.retries ?? this.config.request.retries;
315
+
316
+ this.httpLogger.logSuccess({
317
+ request: {
318
+ method: options.method,
319
+ url: options.url,
320
+ headers: options.headers,
321
+ body: options.body,
322
+ scenario: options.scenario,
323
+ meta: options.meta
324
+ },
325
+ response,
326
+ duration,
327
+ retryCount,
328
+ streamMetrics
329
+ });
330
+ }
331
+
332
+ /**
333
+ * 判断响应数据是否为流对象(Node.js Readable)
334
+ */
335
+ private isStreamResponse(data: any): boolean {
336
+ // undici 和 axios 都返回 Node.js Readable
337
+ return data instanceof Readable;
338
+ }
339
+
340
+ /**
341
+ * 包装流式响应,追踪流的完整生命周期
342
+ */
343
+
344
+ /* eslint-disable max-lines-per-function */
345
+ private wrapStreamResponse<T>(
346
+ options: HttpRequestOptions,
347
+ response: HttpResponse<T>,
348
+ requestStartTime: number,
349
+ ttfb: number
350
+ ): HttpResponse<T> {
351
+ const stream = response.data as any;
352
+ let bytesTransferred = 0;
353
+
354
+ // 记录首次响应日志(流开始)
355
+ this.logResponse(options, response, ttfb, {
356
+ ttfb,
357
+ completed: false
358
+ });
359
+
360
+ this.logger.debug('Stream response started', {
361
+ method: options.method,
362
+ url: options.url,
363
+ ttfb,
364
+ status: response.status
365
+ });
366
+
367
+ // 所有适配器都返回 Node.js Readable
368
+ const originalStream = stream as Readable;
369
+ const wrappedStream = new PassThrough();
370
+
371
+ // 监听数据事件统计字节数
372
+ originalStream.on('data', (chunk: Buffer) => {
373
+ bytesTransferred += chunk.length;
374
+ wrappedStream.write(chunk);
375
+ });
376
+
377
+ // 监听结束事件记录日志
378
+ originalStream.on('end', () => {
379
+ const totalDuration = Date.now() - requestStartTime;
380
+ wrappedStream.end();
381
+
382
+ // 记录流完成日志
383
+ this.logStreamCompleted(
384
+ options,
385
+ response,
386
+ ttfb,
387
+ totalDuration,
388
+ bytesTransferred
389
+ );
390
+ });
391
+
392
+ // 监听错误事件
393
+ originalStream.on('error', (error: Error) => {
394
+ const totalDuration = Date.now() - requestStartTime;
395
+ this.logger.error('Stream read error', {
396
+ method: options.method,
397
+ url: options.url,
398
+ ttfb,
399
+ totalDuration,
400
+ bytesTransferred,
401
+ error: error.message
402
+ });
403
+ wrappedStream.destroy(error);
404
+ });
405
+
406
+ // 使用 pipe 连接流
407
+ originalStream.pipe(wrappedStream, { end: false });
408
+
409
+ // 返回包装后的响应
410
+ return {
411
+ ...response,
412
+ data: wrappedStream as T
413
+ };
414
+ }
415
+
416
+ /**
417
+ * 记录流完成日志
418
+ */
419
+ private logStreamCompleted(
420
+ options: HttpRequestOptions,
421
+ response: HttpResponse,
422
+ ttfb: number,
423
+ totalDuration: number,
424
+ bytesTransferred: number
425
+ ): void {
426
+ // 记录流完成日志
427
+ this.logResponse(options, response, totalDuration, {
428
+ ttfb,
429
+ completed: true,
430
+ totalDuration,
431
+ bytesTransferred
432
+ });
433
+
434
+ this.logger.debug('Stream response completed', {
435
+ method: options.method,
436
+ url: options.url,
437
+ ttfb,
438
+ totalDuration,
439
+ streamDuration: totalDuration - ttfb,
440
+ bytesTransferred,
441
+ throughput: bytesTransferred > 0 && totalDuration > ttfb
442
+ ? `${ (bytesTransferred / 1024 / ((totalDuration - ttfb) / 1000)).toFixed(2) } KB/s`
443
+ : 'N/A'
444
+ });
445
+ }
446
+
447
+ /**
448
+ * 记录错误日志
449
+ */
450
+ private logError(options: HttpRequestOptions, error: any, duration: number): void {
451
+ if (!this.httpLogger) {
452
+ return;
453
+ }
454
+
455
+ // 计算实际重试次数
456
+ const retryCount = options.retries ?? this.config.request.retries;
457
+
458
+ const httpError = error instanceof HttpError ? error : undefined;
459
+
460
+ this.httpLogger.logFailed({
461
+ request: {
462
+ method: options.method,
463
+ url: options.url,
464
+ headers: options.headers,
465
+ body: options.body,
466
+ scenario: options.scenario,
467
+ meta: options.meta
468
+ },
469
+ error: error instanceof Error ? error : new Error(String(error)),
470
+ errorCode: httpError?.code || 'UNKNOWN_ERROR',
471
+ status: httpError?.status,
472
+ response: httpError?.response,
473
+ duration,
474
+ retryCount
475
+ });
476
+ }
477
+
478
+ /**
479
+ * 应用正向代理配置
480
+ * @param options 请求选项
481
+ */
482
+ private applyForwardProxy(options: HttpRequestOptions): void {
483
+ if (!this.forwardProxy || !this.forwardProxy.enabled) {
484
+ return;
485
+ }
486
+
487
+ const { https_proxy, http_proxy, no_proxy } = this.forwardProxy;
488
+
489
+ // 解析目标 URL
490
+ const targetUrl = new URL(options.url);
491
+ const hostname = targetUrl.hostname;
492
+
493
+ // 检查 no_proxy 规则(使用传统匹配模式)
494
+ const noProxyRegExps = DomainUtils.createLegacyDomainMatchers(no_proxy ?? []);
495
+ if (noProxyRegExps.length > 0 && hostname) {
496
+ for (const noProxyRegex of noProxyRegExps) {
497
+ if (noProxyRegex.test(hostname)) {
498
+ this.logger.debug('Skip proxy for no_proxy matched host', {
499
+ hostname,
500
+ pattern: noProxyRegex.source
501
+ });
502
+ return;
503
+ }
504
+ }
505
+ }
506
+
507
+ const protocol = targetUrl.protocol || 'http:';
508
+ const isHttps = /https:?/.test(protocol);
509
+ const proxyUrl = isHttps ? https_proxy : http_proxy;
510
+
511
+ if (!proxyUrl) {
512
+ this.logger.warn('Forward proxy enabled but proxy URL not configured', {
513
+ protocol
514
+ });
515
+ return;
516
+ }
517
+
518
+ // 将代理信息附加到请求元数据中,由适配器处理
519
+ options.meta = {
520
+ ...options.meta,
521
+ __forwardProxy: {
522
+ proxyUrl,
523
+ isHttps,
524
+ protocol
525
+ }
526
+ };
527
+
528
+ this.logger.debug('Applied forward proxy', {
529
+ url: options.url,
530
+ proxyUrl,
531
+ isHttps
532
+ });
533
+ }
534
+
535
+
536
+ /**
537
+ * 清空 DNS 缓存
538
+ */
539
+ public clearDnsCache(): void {
540
+ this.ssrfGuard.clearDnsCache();
541
+ this.logger.debug('DNS cache cleared');
542
+ }
543
+
544
+ /**
545
+ * 获取 DNS 缓存大小
546
+ * @returns 缓存条目数量
547
+ */
548
+ public getDnsCacheSize(): number {
549
+ return this.ssrfGuard.getDnsCacheSize();
550
+ }
551
+ }
552
+
package/src/config.ts ADDED
@@ -0,0 +1,86 @@
1
+ import type {
2
+ HttpClientConfig,
3
+ PartialHttpClientConfig
4
+ } from './types';
5
+ import {
6
+ DEFAULT_HTTP_CLIENT_CONFIG,
7
+ DEFAULT_LOGGING_CONFIG,
8
+ DEFAULT_REQUEST_CONFIG,
9
+ DEFAULT_SECURITY_CONFIG,
10
+ MAX_DNS_CACHE_TTL
11
+ } from './const';
12
+
13
+ /**
14
+ * 合并配置
15
+ * @param userConfig 用户配置
16
+ * @returns 完整配置
17
+ */
18
+ export function mergeConfig(userConfig: PartialHttpClientConfig = {}): HttpClientConfig {
19
+ return {
20
+ adapter: userConfig.adapter ?? DEFAULT_HTTP_CLIENT_CONFIG.adapter,
21
+ security: {
22
+ ...DEFAULT_SECURITY_CONFIG,
23
+ ...userConfig.security
24
+ },
25
+ logging: {
26
+ ...DEFAULT_LOGGING_CONFIG,
27
+ ...userConfig.logging
28
+ },
29
+ request: {
30
+ ...DEFAULT_REQUEST_CONFIG,
31
+ ...userConfig.request
32
+ }
33
+ };
34
+ }
35
+
36
+ /**
37
+ * 验证配置
38
+ * @param config 配置
39
+ * @throws {Error} 配置无效时抛出错误
40
+ */
41
+ export function validateConfig(config: HttpClientConfig): void {
42
+ // 验证适配器
43
+ if (!['axios', 'undici'].includes(config.adapter)) {
44
+ throw new Error(`Invalid adapter: ${ config.adapter }. Must be 'axios' or 'undici'.`);
45
+ }
46
+
47
+ // 验证安全配置
48
+ if (config.security.dnsCacheTtl < 0) {
49
+ throw new Error('DNS cache TTL must be non-negative.');
50
+ }
51
+
52
+ if (config.security.dnsCacheTtl > MAX_DNS_CACHE_TTL) {
53
+ throw new Error(`DNS cache TTL must not exceed ${ MAX_DNS_CACHE_TTL }ms (24 hours) to prevent memory buildup.`);
54
+ }
55
+
56
+ // 验证请求配置
57
+ if (config.request.timeout <= 0) {
58
+ throw new Error('Timeout must be positive.');
59
+ }
60
+
61
+ if (config.request.retries < 0) {
62
+ throw new Error('Retries must be non-negative.');
63
+ }
64
+
65
+ if (config.request.retryDelay < 0) {
66
+ throw new Error('Retry delay must be non-negative.');
67
+ }
68
+
69
+ if (config.request.maxRedirects < 0) {
70
+ throw new Error('Max redirects must be non-negative.');
71
+ }
72
+
73
+ if (config.request.maxRequestBodySize <= 0) {
74
+ throw new Error('Max request body size must be positive.');
75
+ }
76
+
77
+ if (config.request.maxResponseBodySize <= 0) {
78
+ throw new Error('Max response body size must be positive.');
79
+ }
80
+
81
+ // 验证日志配置
82
+ if (config.logging.bodyMaxLength <= 0) {
83
+ throw new Error('Body max length must be positive.');
84
+ }
85
+ }
86
+
@@ -0,0 +1,60 @@
1
+ import type {
2
+ HttpClientConfig,
3
+ LoggingConfig,
4
+ RequestConfig,
5
+ SecurityConfig
6
+ } from '../types';
7
+ import { EClientAdapter } from './enum.const';
8
+ import { DEFAULT_DNS_CACHE_TTL, DEFAULT_DNS_CACHE_MAX_SIZE } from './dns.const';
9
+
10
+ /**
11
+ * DNS 缓存最大 TTL(毫秒)- 5 分钟
12
+ * 避免过长的缓存时间导致内存积压
13
+ */
14
+ export const MAX_DNS_CACHE_TTL = 5 * 60 * 1000; // 300000ms (5 分钟)
15
+
16
+ /**
17
+ * 默认安全配置
18
+ */
19
+ export const DEFAULT_SECURITY_CONFIG: SecurityConfig = {
20
+ rejectUnauthorized: true,
21
+ enableDnsValidation: true,
22
+ enableIpValidation: true,
23
+ allowPrivateIp: false,
24
+ whitelist: [],
25
+ blacklist: [],
26
+ dnsCacheTtl: DEFAULT_DNS_CACHE_TTL,
27
+ dnsCacheMaxSize: DEFAULT_DNS_CACHE_MAX_SIZE
28
+ };
29
+
30
+ /**
31
+ * 默认日志配置
32
+ */
33
+ export const DEFAULT_LOGGING_CONFIG: LoggingConfig = {
34
+ enabled: true,
35
+ logRequestBody: true,
36
+ logResponseBody: true,
37
+ bodyMaxLength: 1024 * 1024 // 1MB
38
+ };
39
+
40
+ /**
41
+ * 默认请求配置
42
+ */
43
+ export const DEFAULT_REQUEST_CONFIG: RequestConfig = {
44
+ timeout: 30000, // 30 秒
45
+ retries: 0,
46
+ retryDelay: 1000, // 1 秒
47
+ maxRedirects: 5,
48
+ maxRequestBodySize: 10 * 1024 * 1024, // 10MB
49
+ maxResponseBodySize: 50 * 1024 * 1024 // 50MB
50
+ };
51
+
52
+ /**
53
+ * 默认 HTTP 客户端配置
54
+ */
55
+ export const DEFAULT_HTTP_CLIENT_CONFIG: HttpClientConfig = {
56
+ adapter: EClientAdapter.axios,
57
+ security: DEFAULT_SECURITY_CONFIG,
58
+ logging: DEFAULT_LOGGING_CONFIG,
59
+ request: DEFAULT_REQUEST_CONFIG
60
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 默认 DNS 缓存 TTL(毫秒)- 60 秒
3
+ */
4
+ export const DEFAULT_DNS_CACHE_TTL = 60000;
5
+
6
+ /**
7
+ * 默认 DNS 解析超时时间(毫秒)- 3 秒
8
+ */
9
+ export const DEFAULT_DNS_RESOLVE_TIMEOUT = 3000;
10
+
11
+ /**
12
+ * 默认 DNS 缓存最大条目数
13
+ */
14
+ export const DEFAULT_DNS_CACHE_MAX_SIZE = 500;
15
+