nstarter-http-request 0.1.12 → 0.1.14
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 +8 -0
- package/dist/cjs/adapter/axios.adapter.js +93 -1
- 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 +85 -73
- 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/const/config.const.js +7 -1
- package/dist/cjs/const/config.const.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/types/errors.js +47 -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 +95 -3
- 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 +86 -74
- 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/const/config.const.js +6 -0
- package/dist/esm/const/config.const.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/esm/types/errors.js +44 -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 +20 -11
- 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/errors.d.ts +39 -1
- package/dist/types/types/errors.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 +124 -3
- package/src/adapter/base.ts +22 -1
- package/src/adapter/undici.adapter.ts +99 -92
- package/src/client.ts +114 -63
- package/src/const/config.const.ts +7 -0
- package/src/types/errors.ts +64 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/redirect.ts +163 -0
|
@@ -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
|
+
}
|