nstarter-http-request 0.1.11 → 0.1.13
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 +6 -0
- package/README.md +1 -2
- package/dist/cjs/adapter/axios.adapter.js +97 -2
- package/dist/cjs/adapter/axios.adapter.js.map +1 -1
- package/dist/cjs/adapter/base.js +20 -1
- package/dist/cjs/adapter/base.js.map +1 -1
- package/dist/cjs/adapter/undici.adapter.js +97 -77
- package/dist/cjs/adapter/undici.adapter.js.map +1 -1
- package/dist/cjs/client.js +99 -56
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/config.js +1 -1
- package/dist/cjs/config.js.map +1 -1
- package/dist/cjs/const/config.const.js +8 -3
- package/dist/cjs/const/config.const.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/types/errors.js +30 -2
- package/dist/cjs/types/errors.js.map +1 -1
- package/dist/cjs/utils/index.js +2 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/redirect.js +138 -0
- package/dist/cjs/utils/redirect.js.map +1 -0
- package/dist/esm/adapter/axios.adapter.js +99 -4
- package/dist/esm/adapter/axios.adapter.js.map +1 -1
- package/dist/esm/adapter/base.js +20 -1
- package/dist/esm/adapter/base.js.map +1 -1
- package/dist/esm/adapter/undici.adapter.js +98 -78
- package/dist/esm/adapter/undici.adapter.js.map +1 -1
- package/dist/esm/client.js +100 -57
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/config.js +1 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/const/config.const.js +7 -2
- package/dist/esm/const/config.const.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/types/errors.js +28 -1
- package/dist/esm/types/errors.js.map +1 -1
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/index.js.map +1 -1
- package/dist/esm/utils/redirect.js +133 -0
- package/dist/esm/utils/redirect.js.map +1 -0
- package/dist/types/adapter/axios.adapter.d.ts +22 -0
- package/dist/types/adapter/axios.adapter.d.ts.map +1 -1
- package/dist/types/adapter/base.d.ts +6 -0
- package/dist/types/adapter/base.d.ts.map +1 -1
- package/dist/types/adapter/undici.adapter.d.ts +24 -12
- package/dist/types/adapter/undici.adapter.d.ts.map +1 -1
- package/dist/types/client.d.ts +11 -0
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/const/config.const.d.ts +6 -0
- package/dist/types/const/config.const.d.ts.map +1 -1
- package/dist/types/types/config.d.ts +2 -2
- package/dist/types/types/config.d.ts.map +1 -1
- package/dist/types/types/errors.d.ts +26 -1
- package/dist/types/types/errors.d.ts.map +1 -1
- package/dist/types/types/request_response.d.ts +1 -1
- package/dist/types/types/request_response.d.ts.map +1 -1
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/index.d.ts.map +1 -1
- package/dist/types/utils/redirect.d.ts +26 -0
- package/dist/types/utils/redirect.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/adapter/axios.adapter.ts +128 -4
- package/src/adapter/base.ts +22 -1
- package/src/adapter/undici.adapter.ts +112 -91
- package/src/client.ts +114 -63
- package/src/config.ts +1 -1
- package/src/const/config.const.ts +8 -2
- package/src/types/config.ts +2 -3
- package/src/types/errors.ts +39 -1
- package/src/types/request_response.ts +1 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/redirect.ts +163 -0
package/src/types/errors.ts
CHANGED
|
@@ -8,12 +8,21 @@ import type { HttpRequestOptions, HttpResponse } from './request_response';
|
|
|
8
8
|
* HTTP 错误
|
|
9
9
|
*/
|
|
10
10
|
export class HttpError extends Error {
|
|
11
|
+
/**
|
|
12
|
+
* @param message 错误信息
|
|
13
|
+
* @param code 错误码
|
|
14
|
+
* @param status HTTP 状态码
|
|
15
|
+
* @param response 响应(不应携带不再需要的大体积响应体)
|
|
16
|
+
* @param request 请求选项
|
|
17
|
+
* @param retriable 是否允许重试,默认 true;重试无法改变结果的确定性失败应传 false
|
|
18
|
+
*/
|
|
11
19
|
constructor(
|
|
12
20
|
message: string,
|
|
13
21
|
public readonly code: string,
|
|
14
22
|
public readonly status?: number,
|
|
15
23
|
public readonly response?: HttpResponse,
|
|
16
|
-
public readonly request?: HttpRequestOptions
|
|
24
|
+
public readonly request?: HttpRequestOptions,
|
|
25
|
+
public readonly retriable = true
|
|
17
26
|
) {
|
|
18
27
|
super(message);
|
|
19
28
|
this.name = 'HttpError';
|
|
@@ -21,6 +30,35 @@ export class HttpError extends Error {
|
|
|
21
30
|
}
|
|
22
31
|
}
|
|
23
32
|
|
|
33
|
+
/**
|
|
34
|
+
* 响应体超过 maxResponseBodySize 限制的错误。
|
|
35
|
+
*
|
|
36
|
+
* - 错误码与文案对齐 axios 的 maxContentLength(ERR_BAD_RESPONSE)。
|
|
37
|
+
* - 重试不会改变响应体大小,因此标记为不可重试,避免每次重试都重复下载。
|
|
38
|
+
* - 不携带 response:仅保留状态码,防止超限数据随错误对象存活(与 axios 抛出该错误时一致)。
|
|
39
|
+
*/
|
|
40
|
+
export class ResponseTooLargeError extends HttpError {
|
|
41
|
+
/**
|
|
42
|
+
* @param maxResponseBodySize 响应体大小上限
|
|
43
|
+
* @param request 请求选项
|
|
44
|
+
* @param status 响应状态码(未知时不传)
|
|
45
|
+
*/
|
|
46
|
+
constructor(
|
|
47
|
+
public readonly maxResponseBodySize: number,
|
|
48
|
+
request: HttpRequestOptions,
|
|
49
|
+
status?: number
|
|
50
|
+
) {
|
|
51
|
+
super(
|
|
52
|
+
`maxContentLength size of ${ maxResponseBodySize } exceeded`,
|
|
53
|
+
'ERR_BAD_RESPONSE',
|
|
54
|
+
status,
|
|
55
|
+
undefined,
|
|
56
|
+
request,
|
|
57
|
+
false
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
24
62
|
/**
|
|
25
63
|
* 安全验证错误
|
|
26
64
|
*/
|
|
@@ -33,7 +33,7 @@ export interface HttpRequestOptions {
|
|
|
33
33
|
params?: Record<string, string | number | boolean>;
|
|
34
34
|
/** 请求体 */
|
|
35
35
|
body?: any;
|
|
36
|
-
/**
|
|
36
|
+
/** 请求超时(毫秒,覆盖全局配置);不配置表示不限制请求超时 */
|
|
37
37
|
timeout?: number;
|
|
38
38
|
/** 重试次数(覆盖全局配置) */
|
|
39
39
|
retries?: number;
|
|
@@ -62,4 +62,3 @@ export interface HttpResponse<T = any> {
|
|
|
62
62
|
/** 请求配置 */
|
|
63
63
|
config: HttpRequestOptions;
|
|
64
64
|
}
|
|
65
|
-
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
HttpRequestOptions,
|
|
3
|
+
HttpResponse
|
|
4
|
+
} from '../types';
|
|
5
|
+
import { HttpError } from '../types';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
import { EClientAdapter } from '../const';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 交给调用方跟跳的状态码,与 undici 的 redirectableStatusCodes 一致。
|
|
11
|
+
* 304 表示未修改,305 表示改走代理,306 已废弃,这三类不跟。
|
|
12
|
+
*/
|
|
13
|
+
const REDIRECT_STATUS: ReadonlySet<number> = new Set([
|
|
14
|
+
300,
|
|
15
|
+
301,
|
|
16
|
+
302,
|
|
17
|
+
303,
|
|
18
|
+
307,
|
|
19
|
+
308
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 判断状态码是否需要跟跳。
|
|
24
|
+
* @param status HTTP 状态码
|
|
25
|
+
*/
|
|
26
|
+
export function isRedirectStatus(status: number): boolean {
|
|
27
|
+
return REDIRECT_STATUS.has(status);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 根据响应里的 Location 构造下一跳请求。
|
|
32
|
+
* 没有 Location 时返回 undefined,调用方把当前 3xx 原样返回。
|
|
33
|
+
* axios:POST 的 301/302,以及非 GET/HEAD 的 303,改成 GET 并去掉 body 和 content-*。
|
|
34
|
+
* undici:只有非 HEAD 的 303 改成 GET 并去掉 body;303 仍去掉 content-*。
|
|
35
|
+
* 两边都去掉 host;跨域去掉 authorization、cookie、proxy-authorization。
|
|
36
|
+
* @param options 当前这一跳的请求
|
|
37
|
+
* @param response 当前这一跳的响应
|
|
38
|
+
* @param adapter 当前适配器
|
|
39
|
+
* @param configuredTimeout 客户端配置中的超时。请求未单独设置时使用。
|
|
40
|
+
*/
|
|
41
|
+
export function buildRedirectOptions(
|
|
42
|
+
options: HttpRequestOptions,
|
|
43
|
+
response: HttpResponse,
|
|
44
|
+
adapter: EClientAdapter,
|
|
45
|
+
configuredTimeout?: number
|
|
46
|
+
): HttpRequestOptions | undefined {
|
|
47
|
+
const location = response.headers?.location;
|
|
48
|
+
if (!location) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const status = response.status;
|
|
52
|
+
const currentUrl = new URL(options.url);
|
|
53
|
+
const nextUrl = new URL(location, currentUrl);
|
|
54
|
+
const headers = { ...options.headers };
|
|
55
|
+
const crossOrigin = currentUrl.origin !== nextUrl.origin;
|
|
56
|
+
const rewriteToGet = shouldRewriteToGet(adapter, status, options.method);
|
|
57
|
+
const removeContentHeaders = adapter === EClientAdapter.axios
|
|
58
|
+
? rewriteToGet
|
|
59
|
+
: status === 303;
|
|
60
|
+
|
|
61
|
+
// 清理请求头
|
|
62
|
+
for (const key of Object.keys(headers)) {
|
|
63
|
+
const name = key.toLowerCase();
|
|
64
|
+
if (
|
|
65
|
+
name === 'host'
|
|
66
|
+
|| (removeContentHeaders && name.startsWith('content-'))
|
|
67
|
+
|| (crossOrigin && (name === 'authorization' || name === 'cookie' || name === 'proxy-authorization'))
|
|
68
|
+
) {
|
|
69
|
+
delete headers[key];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const nextOptions: HttpRequestOptions = {
|
|
73
|
+
...options,
|
|
74
|
+
url: nextUrl.toString(),
|
|
75
|
+
params: undefined,
|
|
76
|
+
headers,
|
|
77
|
+
timeout: options.timeout ?? configuredTimeout,
|
|
78
|
+
meta: options.meta ? { ...options.meta } : undefined
|
|
79
|
+
};
|
|
80
|
+
// 清理 meta 中的正向代理信息
|
|
81
|
+
if (nextOptions.meta) {
|
|
82
|
+
delete nextOptions.meta.__forwardProxy;
|
|
83
|
+
}
|
|
84
|
+
if (rewriteToGet) {
|
|
85
|
+
nextOptions.method = 'GET';
|
|
86
|
+
nextOptions.body = undefined;
|
|
87
|
+
} else if (!isReplayable(options.body)) {
|
|
88
|
+
throw new Error('Request body is not replayable');
|
|
89
|
+
}
|
|
90
|
+
return nextOptions;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* axios 的 301/302 只改 POST。undici 的 301/302 保持原方法。
|
|
95
|
+
* 303 时两边都把非 HEAD 改成 GET;axios 额外保留已经是 GET 的请求。
|
|
96
|
+
*/
|
|
97
|
+
function shouldRewriteToGet(adapter: EClientAdapter, status: number, method: HttpRequestOptions['method']): boolean {
|
|
98
|
+
if (adapter === EClientAdapter.axios) {
|
|
99
|
+
return ((status === 301 || status === 302) && method === 'POST')
|
|
100
|
+
|| (status === 303 && method !== 'GET' && method !== 'HEAD');
|
|
101
|
+
}
|
|
102
|
+
return status === 303 && method !== 'HEAD';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 字符串、Buffer、普通对象每次都能重新序列化。
|
|
107
|
+
* 流和异步可迭代对象读过一次就不能再发。
|
|
108
|
+
*/
|
|
109
|
+
function isReplayable(body: unknown): boolean {
|
|
110
|
+
if (
|
|
111
|
+
body === null
|
|
112
|
+
|| body === undefined
|
|
113
|
+
|| typeof body === 'string'
|
|
114
|
+
|| typeof body === 'number'
|
|
115
|
+
|| typeof body === 'boolean'
|
|
116
|
+
|| Buffer.isBuffer(body)
|
|
117
|
+
|| body instanceof ArrayBuffer
|
|
118
|
+
|| ArrayBuffer.isView(body)
|
|
119
|
+
|| body instanceof URLSearchParams
|
|
120
|
+
|| (typeof Blob !== 'undefined' && body instanceof Blob)
|
|
121
|
+
|| (typeof FormData !== 'undefined' && body instanceof FormData)
|
|
122
|
+
) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (body instanceof Readable) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (typeof body !== 'object') {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const candidate = body as {
|
|
132
|
+
getReader?: unknown,
|
|
133
|
+
[Symbol.asyncIterator]?: unknown
|
|
134
|
+
};
|
|
135
|
+
return typeof candidate.getReader !== 'function'
|
|
136
|
+
&& typeof candidate[Symbol.asyncIterator] !== 'function';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 跟跳共用同一次超时。把本跳已经花掉的时间从 redirectOptions.timeout 扣掉。
|
|
141
|
+
* @param redirectOptions 下一跳请求
|
|
142
|
+
* @param startTime 本跳开始时间
|
|
143
|
+
*/
|
|
144
|
+
export function consumeRedirectTimeout(
|
|
145
|
+
redirectOptions: HttpRequestOptions,
|
|
146
|
+
startTime: number
|
|
147
|
+
): void {
|
|
148
|
+
const timeout = redirectOptions.timeout;
|
|
149
|
+
if (typeof timeout !== 'number' || !Number.isFinite(timeout) || timeout <= 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const remaining = timeout - (Date.now() - startTime);
|
|
153
|
+
if (remaining <= 0) {
|
|
154
|
+
throw new HttpError(
|
|
155
|
+
`timeout of ${ timeout }ms exceeded`,
|
|
156
|
+
'ECONNABORTED',
|
|
157
|
+
undefined,
|
|
158
|
+
undefined,
|
|
159
|
+
redirectOptions
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
redirectOptions.timeout = remaining;
|
|
163
|
+
}
|