nstarter-http-request 0.1.12 → 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/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 +82 -70
- 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 +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 +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 +83 -71
- 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 +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 +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 +26 -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 +95 -83
- package/src/client.ts +114 -63
- package/src/const/config.const.ts +7 -0
- package/src/types/errors.ts +39 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/redirect.ts +163 -0
package/src/client.ts
CHANGED
|
@@ -32,6 +32,7 @@ import { EClientAdapter } from './const';
|
|
|
32
32
|
import { HttpError } from './types';
|
|
33
33
|
import {
|
|
34
34
|
DomainUtils,
|
|
35
|
+
RedirectUtils,
|
|
35
36
|
TraceContextUtils
|
|
36
37
|
} from './utils';
|
|
37
38
|
import {
|
|
@@ -39,6 +40,7 @@ import {
|
|
|
39
40
|
pipeline,
|
|
40
41
|
Readable
|
|
41
42
|
} from 'node:stream';
|
|
43
|
+
import { finished } from 'node:stream/promises';
|
|
42
44
|
import { MetricsTransform } from './stream';
|
|
43
45
|
|
|
44
46
|
type ForwardProxyDecision = {
|
|
@@ -189,84 +191,120 @@ export class NsHttpClient implements IHttpClient {
|
|
|
189
191
|
const startTime = Date.now();
|
|
190
192
|
|
|
191
193
|
try {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
skipDnsValidationReason: forwardProxyDecision.shouldProxy ? 'forward_proxy' : undefined
|
|
200
|
-
}
|
|
201
|
-
);
|
|
194
|
+
return await this.safeRequest<T>(options, this.config.request.maxRedirects);
|
|
195
|
+
} catch (error) {
|
|
196
|
+
// 记录错误日志
|
|
197
|
+
this.logError(options, error, Date.now() - startTime);
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
202
201
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
202
|
+
/**
|
|
203
|
+
*
|
|
204
|
+
* @param options 请求选项
|
|
205
|
+
* @param remainedRedirects 剩余可重定向次数
|
|
206
|
+
* @returns 响应结果
|
|
207
|
+
*/
|
|
208
|
+
private async safeRequest<T>(options: HttpRequestOptions, remainedRedirects: number): Promise<HttpResponse<T>> {
|
|
209
|
+
const startTime = Date.now();
|
|
210
|
+
const forwardProxyDecision = this.resolveForwardProxyDecision(options.url);
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
212
|
+
// 安全验证
|
|
213
|
+
const { safeUrl, result: ssrfResult } = await this.ssrfGuard.validateAndGetSafeUrl(
|
|
214
|
+
options.url,
|
|
215
|
+
{
|
|
216
|
+
skipDnsValidation: forwardProxyDecision.shouldProxy,
|
|
217
|
+
skipDnsValidationReason: forwardProxyDecision.shouldProxy ? 'forward_proxy' : undefined
|
|
217
218
|
}
|
|
219
|
+
);
|
|
218
220
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
this.logger.debug('URL validation passed', {
|
|
222
|
+
originalUrl: options.url,
|
|
223
|
+
safeUrl,
|
|
224
|
+
resolvedIps: ssrfResult.resolvedIps,
|
|
225
|
+
dnsValidationSkipped: ssrfResult.dnsValidationSkipped,
|
|
226
|
+
dnsValidationSkipReason: ssrfResult.dnsValidationSkipReason,
|
|
227
|
+
scenario: options.scenario
|
|
228
|
+
});
|
|
224
229
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
});
|
|
232
|
-
} else {
|
|
233
|
-
safeOptions.headers = {
|
|
234
|
-
...safeOptions.headers,
|
|
235
|
-
traceparent: options.traceparent
|
|
236
|
-
};
|
|
230
|
+
if (ssrfResult.dnsValidationSkipped && forwardProxyDecision.shouldProxy) {
|
|
231
|
+
this.logger.debug('Skipped target DNS validation because request uses forward proxy', {
|
|
232
|
+
hostname: forwardProxyDecision.hostname,
|
|
233
|
+
proxyUrl: forwardProxyDecision.proxyUrl
|
|
234
|
+
});
|
|
235
|
+
}
|
|
237
236
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
237
|
+
// 使用安全的 URL 发起请求
|
|
238
|
+
const safeOptions: HttpRequestOptions = {
|
|
239
|
+
...options,
|
|
240
|
+
url: safeUrl
|
|
241
|
+
};
|
|
244
242
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
243
|
+
// 处理 W3C Trace Context traceparent 头
|
|
244
|
+
if (options.traceparent) {
|
|
245
|
+
// 验证 traceparent 格式
|
|
246
|
+
if (!TraceContextUtils.isValidTraceParent(options.traceparent)) {
|
|
247
|
+
this.logger.warn('Invalid traceparent format, ignoring', {
|
|
248
|
+
traceparent: options.traceparent
|
|
249
|
+
});
|
|
250
|
+
} else {
|
|
251
|
+
safeOptions.headers = {
|
|
252
|
+
...safeOptions.headers,
|
|
253
|
+
traceparent: options.traceparent
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
this.logger.debug('Traceparent header added', {
|
|
257
|
+
traceparent: options.traceparent,
|
|
258
|
+
sampled: TraceContextUtils.isSampled(options.traceparent)
|
|
259
|
+
});
|
|
248
260
|
}
|
|
261
|
+
}
|
|
249
262
|
|
|
250
|
-
|
|
251
|
-
|
|
263
|
+
// 应用正向代理配置
|
|
264
|
+
if (this.forwardProxy) {
|
|
265
|
+
this.applyForwardProxy(safeOptions, forwardProxyDecision);
|
|
266
|
+
}
|
|
252
267
|
|
|
253
|
-
|
|
254
|
-
|
|
268
|
+
// 发起请求
|
|
269
|
+
const response = await this.adapter.request<T>(safeOptions);
|
|
255
270
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
271
|
+
// 需要重定向时,继续跟跳
|
|
272
|
+
if (RedirectUtils.isRedirectStatus(response.status)) {
|
|
273
|
+
try {
|
|
274
|
+
if (remainedRedirects === undefined || remainedRedirects <= 0) {
|
|
275
|
+
throw new Error('max redirects');
|
|
276
|
+
}
|
|
277
|
+
const redirectOptions = RedirectUtils.buildRedirectOptions(
|
|
278
|
+
safeOptions,
|
|
279
|
+
response,
|
|
280
|
+
this.adapter.name,
|
|
281
|
+
this.config.request.timeout
|
|
282
|
+
);
|
|
283
|
+
if (redirectOptions) {
|
|
284
|
+
RedirectUtils.consumeRedirectTimeout(redirectOptions, startTime);
|
|
285
|
+
// 下一跳前,把中间响应的流先丢掉,以免占用连接
|
|
286
|
+
await this.discardResponseStream(response);
|
|
287
|
+
const nextRemainedRedirects = remainedRedirects - 1;
|
|
288
|
+
return await this.safeRequest<T>(redirectOptions, nextRemainedRedirects);
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
await this.discardResponseStream(response);
|
|
292
|
+
throw error;
|
|
259
293
|
}
|
|
294
|
+
}
|
|
260
295
|
|
|
261
|
-
|
|
262
|
-
|
|
296
|
+
// 首次响应时间(TTFB)
|
|
297
|
+
const ttfb = Date.now() - startTime;
|
|
263
298
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
this.logError(options, error, Date.now() - startTime);
|
|
268
|
-
throw error;
|
|
299
|
+
// 如果是流式响应,包装流对象以追踪完整生命周期
|
|
300
|
+
if (options.responseType === 'stream' && this.isStreamResponse(response.data)) {
|
|
301
|
+
return this.wrapStreamResponse(safeOptions, response, startTime, ttfb);
|
|
269
302
|
}
|
|
303
|
+
|
|
304
|
+
// 记录非流式响应日志
|
|
305
|
+
this.logResponse(safeOptions, response, ttfb);
|
|
306
|
+
|
|
307
|
+
return response;
|
|
270
308
|
}
|
|
271
309
|
|
|
272
310
|
/**
|
|
@@ -354,6 +392,19 @@ export class NsHttpClient implements IHttpClient {
|
|
|
354
392
|
});
|
|
355
393
|
}
|
|
356
394
|
|
|
395
|
+
/**
|
|
396
|
+
* 销毁不再交给调用方的响应流
|
|
397
|
+
*/
|
|
398
|
+
private async discardResponseStream(response: HttpResponse): Promise<void> {
|
|
399
|
+
const data = response.data;
|
|
400
|
+
if (!(data instanceof Readable) || data.destroyed) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const done = finished(data);
|
|
404
|
+
data.destroy();
|
|
405
|
+
await done.catch(() => undefined);
|
|
406
|
+
}
|
|
407
|
+
|
|
357
408
|
/**
|
|
358
409
|
* 判断响应数据是否为流对象(Node.js Readable)
|
|
359
410
|
*/
|
|
@@ -48,6 +48,13 @@ export const DEFAULT_REQUEST_CONFIG: RequestConfig = {
|
|
|
48
48
|
maxResponseBodySize: 50 * 1024 * 1024 // 50MB
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 响应体大小预检的线上长度容差比例。
|
|
53
|
+
* axios 的 onDownloadProgress.total 是压缩后的 Content-Length,而 maxContentLength 按解压后计数;
|
|
54
|
+
* 不可压缩数据经 gzip 等压缩后可能略大于解压大小,预检仅在超过上限该比例时提前中止。
|
|
55
|
+
*/
|
|
56
|
+
export const MAX_RESPONSE_SIZE_TOLERANCE_RATIO = 0.01;
|
|
57
|
+
|
|
51
58
|
/**
|
|
52
59
|
* 默认 HTTP 客户端配置
|
|
53
60
|
*/
|
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
|
*/
|
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
|
+
}
|