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.
Files changed (72) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -2
  3. package/dist/cjs/adapter/axios.adapter.js +97 -2
  4. package/dist/cjs/adapter/axios.adapter.js.map +1 -1
  5. package/dist/cjs/adapter/base.js +20 -1
  6. package/dist/cjs/adapter/base.js.map +1 -1
  7. package/dist/cjs/adapter/undici.adapter.js +97 -77
  8. package/dist/cjs/adapter/undici.adapter.js.map +1 -1
  9. package/dist/cjs/client.js +99 -56
  10. package/dist/cjs/client.js.map +1 -1
  11. package/dist/cjs/config.js +1 -1
  12. package/dist/cjs/config.js.map +1 -1
  13. package/dist/cjs/const/config.const.js +8 -3
  14. package/dist/cjs/const/config.const.js.map +1 -1
  15. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  16. package/dist/cjs/types/errors.js +30 -2
  17. package/dist/cjs/types/errors.js.map +1 -1
  18. package/dist/cjs/utils/index.js +2 -1
  19. package/dist/cjs/utils/index.js.map +1 -1
  20. package/dist/cjs/utils/redirect.js +138 -0
  21. package/dist/cjs/utils/redirect.js.map +1 -0
  22. package/dist/esm/adapter/axios.adapter.js +99 -4
  23. package/dist/esm/adapter/axios.adapter.js.map +1 -1
  24. package/dist/esm/adapter/base.js +20 -1
  25. package/dist/esm/adapter/base.js.map +1 -1
  26. package/dist/esm/adapter/undici.adapter.js +98 -78
  27. package/dist/esm/adapter/undici.adapter.js.map +1 -1
  28. package/dist/esm/client.js +100 -57
  29. package/dist/esm/client.js.map +1 -1
  30. package/dist/esm/config.js +1 -1
  31. package/dist/esm/config.js.map +1 -1
  32. package/dist/esm/const/config.const.js +7 -2
  33. package/dist/esm/const/config.const.js.map +1 -1
  34. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  35. package/dist/esm/types/errors.js +28 -1
  36. package/dist/esm/types/errors.js.map +1 -1
  37. package/dist/esm/utils/index.js +1 -0
  38. package/dist/esm/utils/index.js.map +1 -1
  39. package/dist/esm/utils/redirect.js +133 -0
  40. package/dist/esm/utils/redirect.js.map +1 -0
  41. package/dist/types/adapter/axios.adapter.d.ts +22 -0
  42. package/dist/types/adapter/axios.adapter.d.ts.map +1 -1
  43. package/dist/types/adapter/base.d.ts +6 -0
  44. package/dist/types/adapter/base.d.ts.map +1 -1
  45. package/dist/types/adapter/undici.adapter.d.ts +24 -12
  46. package/dist/types/adapter/undici.adapter.d.ts.map +1 -1
  47. package/dist/types/client.d.ts +11 -0
  48. package/dist/types/client.d.ts.map +1 -1
  49. package/dist/types/const/config.const.d.ts +6 -0
  50. package/dist/types/const/config.const.d.ts.map +1 -1
  51. package/dist/types/types/config.d.ts +2 -2
  52. package/dist/types/types/config.d.ts.map +1 -1
  53. package/dist/types/types/errors.d.ts +26 -1
  54. package/dist/types/types/errors.d.ts.map +1 -1
  55. package/dist/types/types/request_response.d.ts +1 -1
  56. package/dist/types/types/request_response.d.ts.map +1 -1
  57. package/dist/types/utils/index.d.ts +1 -0
  58. package/dist/types/utils/index.d.ts.map +1 -1
  59. package/dist/types/utils/redirect.d.ts +26 -0
  60. package/dist/types/utils/redirect.d.ts.map +1 -0
  61. package/package.json +1 -1
  62. package/src/adapter/axios.adapter.ts +128 -4
  63. package/src/adapter/base.ts +22 -1
  64. package/src/adapter/undici.adapter.ts +112 -91
  65. package/src/client.ts +114 -63
  66. package/src/config.ts +1 -1
  67. package/src/const/config.const.ts +8 -2
  68. package/src/types/config.ts +2 -3
  69. package/src/types/errors.ts +39 -1
  70. package/src/types/request_response.ts +1 -2
  71. package/src/utils/index.ts +1 -0
  72. package/src/utils/redirect.ts +163 -0
@@ -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
-
@@ -1,6 +1,7 @@
1
1
  export * as IpRangeUtils from './ip.range';
2
2
  export * as CommonUtils from './common';
3
3
  export * as DomainUtils from './domain';
4
+ export * as RedirectUtils from './redirect';
4
5
  export * as TraceContextUtils from './trace.context';
5
6
 
6
7
  // 导出 TraceContext 类供外部直接使用
@@ -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
+ }