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.
- package/CHANGELOG.md +5 -0
- package/README.md +160 -0
- package/dist/cjs/adapter/axios.adapter.js +224 -0
- package/dist/cjs/adapter/axios.adapter.js.map +1 -0
- package/dist/cjs/adapter/base.js +134 -0
- package/dist/cjs/adapter/base.js.map +1 -0
- package/dist/cjs/adapter/index.js +20 -0
- package/dist/cjs/adapter/index.js.map +1 -0
- package/dist/cjs/adapter/undici.adapter.js +272 -0
- package/dist/cjs/adapter/undici.adapter.js.map +1 -0
- package/dist/cjs/client.js +409 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/config.js +69 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/const/config.const.js +53 -0
- package/dist/cjs/const/config.const.js.map +1 -0
- package/dist/cjs/const/dns.const.js +16 -0
- package/dist/cjs/const/dns.const.js.map +1 -0
- package/dist/cjs/const/enum.const.js +22 -0
- package/dist/cjs/const/enum.const.js.map +1 -0
- package/dist/cjs/const/index.js +21 -0
- package/dist/cjs/const/index.js.map +1 -0
- package/dist/cjs/const/ip.const.js +110 -0
- package/dist/cjs/const/ip.const.js.map +1 -0
- package/dist/cjs/index.js +25 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/logger/base.js +60 -0
- package/dist/cjs/logger/base.js.map +1 -0
- package/dist/cjs/logger/http_client.logger.js +209 -0
- package/dist/cjs/logger/http_client.logger.js.map +1 -0
- package/dist/cjs/logger/index.js +20 -0
- package/dist/cjs/logger/index.js.map +1 -0
- package/dist/cjs/logger/log.filter.js +126 -0
- package/dist/cjs/logger/log.filter.js.map +1 -0
- package/dist/cjs/security/dns.validator.js +137 -0
- package/dist/cjs/security/dns.validator.js.map +1 -0
- package/dist/cjs/security/index.js +21 -0
- package/dist/cjs/security/index.js.map +1 -0
- package/dist/cjs/security/ip.validator.js +107 -0
- package/dist/cjs/security/ip.validator.js.map +1 -0
- package/dist/cjs/security/ssrf.guard.js +180 -0
- package/dist/cjs/security/ssrf.guard.js.map +1 -0
- package/dist/cjs/security/url.validator.js +170 -0
- package/dist/cjs/security/url.validator.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -0
- package/dist/cjs/types/adapter.js +6 -0
- package/dist/cjs/types/adapter.js.map +1 -0
- package/dist/cjs/types/client.js +6 -0
- package/dist/cjs/types/client.js.map +1 -0
- package/dist/cjs/types/config.js +6 -0
- package/dist/cjs/types/config.js.map +1 -0
- package/dist/cjs/types/errors.js +35 -0
- package/dist/cjs/types/errors.js.map +1 -0
- package/dist/cjs/types/index.js +35 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/types/ip.js +6 -0
- package/dist/cjs/types/ip.js.map +1 -0
- package/dist/cjs/types/logger.js +6 -0
- package/dist/cjs/types/logger.js.map +1 -0
- package/dist/cjs/types/request_response.js +6 -0
- package/dist/cjs/types/request_response.js.map +1 -0
- package/dist/cjs/types/security.js +6 -0
- package/dist/cjs/types/security.js.map +1 -0
- package/dist/cjs/types/trace.js +14 -0
- package/dist/cjs/types/trace.js.map +1 -0
- package/dist/cjs/utils/common.js +31 -0
- package/dist/cjs/utils/common.js.map +1 -0
- package/dist/cjs/utils/domain.js +79 -0
- package/dist/cjs/utils/domain.js.map +1 -0
- package/dist/cjs/utils/index.js +44 -0
- package/dist/cjs/utils/index.js.map +1 -0
- package/dist/cjs/utils/ip.range.js +200 -0
- package/dist/cjs/utils/ip.range.js.map +1 -0
- package/dist/cjs/utils/trace.context.js +213 -0
- package/dist/cjs/utils/trace.context.js.map +1 -0
- package/dist/esm/adapter/axios.adapter.js +184 -0
- package/dist/esm/adapter/axios.adapter.js.map +1 -0
- package/dist/esm/adapter/base.js +130 -0
- package/dist/esm/adapter/base.js.map +1 -0
- package/dist/esm/adapter/index.js +4 -0
- package/dist/esm/adapter/index.js.map +1 -0
- package/dist/esm/adapter/undici.adapter.js +235 -0
- package/dist/esm/adapter/undici.adapter.js.map +1 -0
- package/dist/esm/client.js +405 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/config.js +65 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/const/config.const.js +50 -0
- package/dist/esm/const/config.const.js.map +1 -0
- package/dist/esm/const/dns.const.js +13 -0
- package/dist/esm/const/dns.const.js.map +1 -0
- package/dist/esm/const/enum.const.js +19 -0
- package/dist/esm/const/enum.const.js.map +1 -0
- package/dist/esm/const/index.js +5 -0
- package/dist/esm/const/index.js.map +1 -0
- package/dist/esm/const/ip.const.js +107 -0
- package/dist/esm/const/ip.const.js.map +1 -0
- package/dist/esm/index.js +9 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/logger/base.js +55 -0
- package/dist/esm/logger/base.js.map +1 -0
- package/dist/esm/logger/http_client.logger.js +205 -0
- package/dist/esm/logger/http_client.logger.js.map +1 -0
- package/dist/esm/logger/index.js +4 -0
- package/dist/esm/logger/index.js.map +1 -0
- package/dist/esm/logger/log.filter.js +122 -0
- package/dist/esm/logger/log.filter.js.map +1 -0
- package/dist/esm/security/dns.validator.js +133 -0
- package/dist/esm/security/dns.validator.js.map +1 -0
- package/dist/esm/security/index.js +5 -0
- package/dist/esm/security/index.js.map +1 -0
- package/dist/esm/security/ip.validator.js +103 -0
- package/dist/esm/security/ip.validator.js.map +1 -0
- package/dist/esm/security/ssrf.guard.js +176 -0
- package/dist/esm/security/ssrf.guard.js.map +1 -0
- package/dist/esm/security/url.validator.js +166 -0
- package/dist/esm/security/url.validator.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/esm/types/adapter.js +5 -0
- package/dist/esm/types/adapter.js.map +1 -0
- package/dist/esm/types/client.js +5 -0
- package/dist/esm/types/client.js.map +1 -0
- package/dist/esm/types/config.js +5 -0
- package/dist/esm/types/config.js.map +1 -0
- package/dist/esm/types/errors.js +30 -0
- package/dist/esm/types/errors.js.map +1 -0
- package/dist/esm/types/index.js +19 -0
- package/dist/esm/types/index.js.map +1 -0
- package/dist/esm/types/ip.js +5 -0
- package/dist/esm/types/ip.js.map +1 -0
- package/dist/esm/types/logger.js +5 -0
- package/dist/esm/types/logger.js.map +1 -0
- package/dist/esm/types/request_response.js +5 -0
- package/dist/esm/types/request_response.js.map +1 -0
- package/dist/esm/types/security.js +5 -0
- package/dist/esm/types/security.js.map +1 -0
- package/dist/esm/types/trace.js +11 -0
- package/dist/esm/types/trace.js.map +1 -0
- package/dist/esm/utils/common.js +27 -0
- package/dist/esm/utils/common.js.map +1 -0
- package/dist/esm/utils/domain.js +71 -0
- package/dist/esm/utils/domain.js.map +1 -0
- package/dist/esm/utils/index.js +7 -0
- package/dist/esm/utils/index.js.map +1 -0
- package/dist/esm/utils/ip.range.js +187 -0
- package/dist/esm/utils/ip.range.js.map +1 -0
- package/dist/esm/utils/trace.context.js +199 -0
- package/dist/esm/utils/trace.context.js.map +1 -0
- package/dist/types/adapter/axios.adapter.d.ts +51 -0
- package/dist/types/adapter/axios.adapter.d.ts.map +1 -0
- package/dist/types/adapter/base.d.ts +56 -0
- package/dist/types/adapter/base.d.ts.map +1 -0
- package/dist/types/adapter/index.d.ts +4 -0
- package/dist/types/adapter/index.d.ts.map +1 -0
- package/dist/types/adapter/undici.adapter.d.ts +68 -0
- package/dist/types/adapter/undici.adapter.d.ts.map +1 -0
- package/dist/types/client.d.ts +105 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/config.d.ts +14 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/const/config.const.d.ts +23 -0
- package/dist/types/const/config.const.d.ts.map +1 -0
- package/dist/types/const/dns.const.d.ts +13 -0
- package/dist/types/const/dns.const.d.ts.map +1 -0
- package/dist/types/const/enum.const.d.ts +17 -0
- package/dist/types/const/enum.const.d.ts.map +1 -0
- package/dist/types/const/index.d.ts +5 -0
- package/dist/types/const/index.d.ts.map +1 -0
- package/dist/types/const/ip.const.d.ts +42 -0
- package/dist/types/const/ip.const.d.ts.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/logger/base.d.ts +42 -0
- package/dist/types/logger/base.d.ts.map +1 -0
- package/dist/types/logger/http_client.logger.d.ts +49 -0
- package/dist/types/logger/http_client.logger.d.ts.map +1 -0
- package/dist/types/logger/index.d.ts +4 -0
- package/dist/types/logger/index.d.ts.map +1 -0
- package/dist/types/logger/log.filter.d.ts +56 -0
- package/dist/types/logger/log.filter.d.ts.map +1 -0
- package/dist/types/security/dns.validator.d.ts +61 -0
- package/dist/types/security/dns.validator.d.ts.map +1 -0
- package/dist/types/security/index.d.ts +5 -0
- package/dist/types/security/index.d.ts.map +1 -0
- package/dist/types/security/ip.validator.d.ts +31 -0
- package/dist/types/security/ip.validator.d.ts.map +1 -0
- package/dist/types/security/ssrf.guard.d.ts +54 -0
- package/dist/types/security/ssrf.guard.d.ts.map +1 -0
- package/dist/types/security/url.validator.d.ts +76 -0
- package/dist/types/security/url.validator.d.ts.map +1 -0
- package/dist/types/types/adapter.d.ts +30 -0
- package/dist/types/types/adapter.d.ts.map +1 -0
- package/dist/types/types/client.d.ts +85 -0
- package/dist/types/types/client.d.ts.map +1 -0
- package/dist/types/types/config.d.ts +99 -0
- package/dist/types/types/config.d.ts.map +1 -0
- package/dist/types/types/errors.d.ts +23 -0
- package/dist/types/types/errors.d.ts.map +1 -0
- package/dist/types/types/index.d.ts +10 -0
- package/dist/types/types/index.d.ts.map +1 -0
- package/dist/types/types/ip.d.ts +32 -0
- package/dist/types/types/ip.d.ts.map +1 -0
- package/dist/types/types/logger.d.ts +136 -0
- package/dist/types/types/logger.d.ts.map +1 -0
- package/dist/types/types/request_response.d.ts +54 -0
- package/dist/types/types/request_response.d.ts.map +1 -0
- package/dist/types/types/security.d.ts +115 -0
- package/dist/types/types/security.d.ts.map +1 -0
- package/dist/types/types/trace.d.ts +34 -0
- package/dist/types/types/trace.d.ts.map +1 -0
- package/dist/types/utils/common.d.ts +14 -0
- package/dist/types/utils/common.d.ts.map +1 -0
- package/dist/types/utils/domain.d.ts +39 -0
- package/dist/types/utils/domain.d.ts.map +1 -0
- package/dist/types/utils/index.d.ts +6 -0
- package/dist/types/utils/index.d.ts.map +1 -0
- package/dist/types/utils/ip.range.d.ts +61 -0
- package/dist/types/utils/ip.range.d.ts.map +1 -0
- package/dist/types/utils/trace.context.d.ts +106 -0
- package/dist/types/utils/trace.context.d.ts.map +1 -0
- package/docs/adapters.md +53 -0
- package/docs/configuration.md +149 -0
- package/docs/logging.md +70 -0
- package/docs/proxy.md +44 -0
- package/docs/security.md +56 -0
- package/docs/trace-context.md +436 -0
- package/package.json +50 -0
- package/src/adapter/axios.adapter.ts +228 -0
- package/src/adapter/base.ts +180 -0
- package/src/adapter/index.ts +3 -0
- package/src/adapter/undici.adapter.ts +282 -0
- package/src/client.ts +552 -0
- package/src/config.ts +86 -0
- package/src/const/config.const.ts +60 -0
- package/src/const/dns.const.ts +15 -0
- package/src/const/enum.const.ts +17 -0
- package/src/const/index.ts +4 -0
- package/src/const/ip.const.ts +139 -0
- package/src/index.ts +8 -0
- package/src/logger/base.ts +75 -0
- package/src/logger/http_client.logger.ts +272 -0
- package/src/logger/index.ts +3 -0
- package/src/logger/log.filter.ts +149 -0
- package/src/security/dns.validator.ts +170 -0
- package/src/security/index.ts +4 -0
- package/src/security/ip.validator.ts +124 -0
- package/src/security/ssrf.guard.ts +224 -0
- package/src/security/url.validator.ts +192 -0
- package/src/types/adapter.ts +38 -0
- package/src/types/client.ts +119 -0
- package/src/types/config.ts +110 -0
- package/src/types/errors.ts +38 -0
- package/src/types/index.ts +27 -0
- package/src/types/ip.ts +34 -0
- package/src/types/logger.ts +150 -0
- package/src/types/request_response.ts +65 -0
- package/src/types/security.ts +126 -0
- package/src/types/trace.ts +35 -0
- package/src/utils/common.ts +28 -0
- package/src/utils/domain.ts +78 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/ip.range.ts +218 -0
- 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,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
|
+
|