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.
Files changed (60) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/cjs/adapter/axios.adapter.js +93 -1
  3. package/dist/cjs/adapter/axios.adapter.js.map +1 -1
  4. package/dist/cjs/adapter/base.js +20 -1
  5. package/dist/cjs/adapter/base.js.map +1 -1
  6. package/dist/cjs/adapter/undici.adapter.js +82 -70
  7. package/dist/cjs/adapter/undici.adapter.js.map +1 -1
  8. package/dist/cjs/client.js +99 -56
  9. package/dist/cjs/client.js.map +1 -1
  10. package/dist/cjs/const/config.const.js +7 -1
  11. package/dist/cjs/const/config.const.js.map +1 -1
  12. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  13. package/dist/cjs/types/errors.js +30 -2
  14. package/dist/cjs/types/errors.js.map +1 -1
  15. package/dist/cjs/utils/index.js +2 -1
  16. package/dist/cjs/utils/index.js.map +1 -1
  17. package/dist/cjs/utils/redirect.js +138 -0
  18. package/dist/cjs/utils/redirect.js.map +1 -0
  19. package/dist/esm/adapter/axios.adapter.js +95 -3
  20. package/dist/esm/adapter/axios.adapter.js.map +1 -1
  21. package/dist/esm/adapter/base.js +20 -1
  22. package/dist/esm/adapter/base.js.map +1 -1
  23. package/dist/esm/adapter/undici.adapter.js +83 -71
  24. package/dist/esm/adapter/undici.adapter.js.map +1 -1
  25. package/dist/esm/client.js +100 -57
  26. package/dist/esm/client.js.map +1 -1
  27. package/dist/esm/const/config.const.js +6 -0
  28. package/dist/esm/const/config.const.js.map +1 -1
  29. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  30. package/dist/esm/types/errors.js +28 -1
  31. package/dist/esm/types/errors.js.map +1 -1
  32. package/dist/esm/utils/index.js +1 -0
  33. package/dist/esm/utils/index.js.map +1 -1
  34. package/dist/esm/utils/redirect.js +133 -0
  35. package/dist/esm/utils/redirect.js.map +1 -0
  36. package/dist/types/adapter/axios.adapter.d.ts +22 -0
  37. package/dist/types/adapter/axios.adapter.d.ts.map +1 -1
  38. package/dist/types/adapter/base.d.ts +6 -0
  39. package/dist/types/adapter/base.d.ts.map +1 -1
  40. package/dist/types/adapter/undici.adapter.d.ts +20 -11
  41. package/dist/types/adapter/undici.adapter.d.ts.map +1 -1
  42. package/dist/types/client.d.ts +11 -0
  43. package/dist/types/client.d.ts.map +1 -1
  44. package/dist/types/const/config.const.d.ts +6 -0
  45. package/dist/types/const/config.const.d.ts.map +1 -1
  46. package/dist/types/types/errors.d.ts +26 -1
  47. package/dist/types/types/errors.d.ts.map +1 -1
  48. package/dist/types/utils/index.d.ts +1 -0
  49. package/dist/types/utils/index.d.ts.map +1 -1
  50. package/dist/types/utils/redirect.d.ts +26 -0
  51. package/dist/types/utils/redirect.d.ts.map +1 -0
  52. package/package.json +1 -1
  53. package/src/adapter/axios.adapter.ts +124 -3
  54. package/src/adapter/base.ts +22 -1
  55. package/src/adapter/undici.adapter.ts +95 -83
  56. package/src/client.ts +114 -63
  57. package/src/const/config.const.ts +7 -0
  58. package/src/types/errors.ts +39 -1
  59. package/src/utils/index.ts +1 -0
  60. package/src/utils/redirect.ts +163 -0
@@ -47,6 +47,13 @@ export declare class NsHttpClient implements IHttpClient {
47
47
  * @returns 响应结果
48
48
  */
49
49
  request<T = any>(options: HttpRequestOptions): Promise<HttpResponse<T>>;
50
+ /**
51
+ *
52
+ * @param options 请求选项
53
+ * @param remainedRedirects 剩余可重定向次数
54
+ * @returns 响应结果
55
+ */
56
+ private safeRequest;
50
57
  /**
51
58
  * 便捷方法:GET 请求
52
59
  */
@@ -71,6 +78,10 @@ export declare class NsHttpClient implements IHttpClient {
71
78
  * 记录响应日志
72
79
  */
73
80
  private logResponse;
81
+ /**
82
+ * 销毁不再交给调用方的响应流
83
+ */
84
+ private discardResponseStream;
74
85
  /**
75
86
  * 判断响应数据是否为流对象(Node.js Readable)
76
87
  */
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAER,uBAAuB,EACvB,kBAAkB,EAClB,YAAY,EAEZ,WAAW,EAGX,aAAa,EAGhB,MAAM,SAAS,CAAC;AAcjB,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AA2BzC;;GAEG;AACH,qBAAa,YAAa,YAAW,WAAW;IAC5C,OAAO,CAAC,MAAM,CAAmB;IACjC,OAAO,CAAC,OAAO,CAAe;IAC9B,OAAO,CAAC,SAAS,CAAa;IAC9B,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,UAAU,CAAC,CAAoB;IACvC,OAAO,CAAC,YAAY,CAAC,CAAgB;gBAEzB,UAAU,GAAE,uBAA4B;IA2BpD;;;OAGG;WACW,oBAAoB,CAAC,MAAM,GAAE,uBAA4B,GAAG,YAAY;IAItF;;;OAGG;IACI,WAAW,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI;IAMjD;;;OAGG;IACI,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAWnD;;OAEG;IACH,OAAO,CAAC,UAAU;IAQlB;;OAEG;IACH,OAAO,CAAC,cAAc;IAsBtB;;OAEG;IACH,OAAO,CAAC,WAAW;IAenB;;;;OAIG;IACU,OAAO,CAAC,CAAC,GAAG,GAAG,EAAE,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAoFpF;;OAEG;IACU,GAAG,CAAC,CAAC,GAAG,GAAG,EACpB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,CAAC,CAAC,GAC9D,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAI3B;;OAEG;IACU,IAAI,CAAC,CAAC,GAAG,GAAG,EACrB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,GAAG,EACV,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC,GACvE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAI3B;;OAEG;IACU,GAAG,CAAC,CAAC,GAAG,GAAG,EACpB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,GAAG,EACV,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC,GACvE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAI3B;;OAEG;IACU,MAAM,CAAC,CAAC,GAAG,GAAG,EACvB,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,CAAC,CAAC,GAC9D,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAI3B;;OAEG;IACU,KAAK,CAAC,CAAC,GAAG,GAAG,EACtB,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,GAAG,EACV,OAAO,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC,GACvE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAI3B;;OAEG;IACH,OAAO,CAAC,WAAW;IA6BnB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAKxB;;OAEG;IAGH,OAAO,CAAC,kBAAkB;IA+D1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IA4B1B;;OAEG;IACH,OAAO,CAAC,QAAQ;IA4BhB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAOzB;;;OAGG;IACH,OAAO,CAAC,2BAA2B;IAqEnC;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAsCjC;;OAEG;IACI,aAAa,IAAI,IAAI;IAK5B;;;OAGG;IACI,eAAe,IAAI,MAAM;CAGnC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/client.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,12 @@ export declare const DEFAULT_LOGGING_CONFIG: LoggingConfig;
16
16
  * 默认请求配置
17
17
  */
18
18
  export declare const DEFAULT_REQUEST_CONFIG: RequestConfig;
19
+ /**
20
+ * 响应体大小预检的线上长度容差比例。
21
+ * axios 的 onDownloadProgress.total 是压缩后的 Content-Length,而 maxContentLength 按解压后计数;
22
+ * 不可压缩数据经 gzip 等压缩后可能略大于解压大小,预检仅在超过上限该比例时提前中止。
23
+ */
24
+ export declare const MAX_RESPONSE_SIZE_TOLERANCE_RATIO = 0.01;
19
25
  /**
20
26
  * 默认 HTTP 客户端配置
21
27
  */
@@ -1 +1 @@
1
- {"version":3,"file":"config.const.d.ts","sourceRoot":"","sources":["../../../src/const/config.const.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,cAAc,EACjB,MAAM,UAAU,CAAC;AAIlB;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAAgB,CAAC;AAE/C;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,cASrC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAKpC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAMpC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,0BAA0B,EAAE,gBAKxC,CAAC"}
1
+ {"version":3,"file":"config.const.d.ts","sourceRoot":"","sources":["../../../src/const/config.const.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,gBAAgB,EAChB,aAAa,EACb,aAAa,EACb,cAAc,EACjB,MAAM,UAAU,CAAC;AAIlB;;;GAGG;AACH,eAAO,MAAM,iBAAiB,QAAgB,CAAC;AAE/C;;GAEG;AACH,eAAO,MAAM,uBAAuB,EAAE,cASrC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAKpC,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,EAAE,aAMpC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iCAAiC,OAAO,CAAC;AAEtD;;GAEG;AACH,eAAO,MAAM,0BAA0B,EAAE,gBAKxC,CAAC"}
@@ -10,7 +10,32 @@ export declare class HttpError extends Error {
10
10
  readonly status?: number | undefined;
11
11
  readonly response?: HttpResponse | undefined;
12
12
  readonly request?: HttpRequestOptions | undefined;
13
- constructor(message: string, code: string, status?: number | undefined, response?: HttpResponse | undefined, request?: HttpRequestOptions | undefined);
13
+ readonly retriable: boolean;
14
+ /**
15
+ * @param message 错误信息
16
+ * @param code 错误码
17
+ * @param status HTTP 状态码
18
+ * @param response 响应(不应携带不再需要的大体积响应体)
19
+ * @param request 请求选项
20
+ * @param retriable 是否允许重试,默认 true;重试无法改变结果的确定性失败应传 false
21
+ */
22
+ constructor(message: string, code: string, status?: number | undefined, response?: HttpResponse | undefined, request?: HttpRequestOptions | undefined, retriable?: boolean);
23
+ }
24
+ /**
25
+ * 响应体超过 maxResponseBodySize 限制的错误。
26
+ *
27
+ * - 错误码与文案对齐 axios 的 maxContentLength(ERR_BAD_RESPONSE)。
28
+ * - 重试不会改变响应体大小,因此标记为不可重试,避免每次重试都重复下载。
29
+ * - 不携带 response:仅保留状态码,防止超限数据随错误对象存活(与 axios 抛出该错误时一致)。
30
+ */
31
+ export declare class ResponseTooLargeError extends HttpError {
32
+ readonly maxResponseBodySize: number;
33
+ /**
34
+ * @param maxResponseBodySize 响应体大小上限
35
+ * @param request 请求选项
36
+ * @param status 响应状态码(未知时不传)
37
+ */
38
+ constructor(maxResponseBodySize: number, request: HttpRequestOptions, status?: number);
14
39
  }
15
40
  /**
16
41
  * 安全验证错误
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/types/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE3E;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;aAGZ,IAAI,EAAE,MAAM;aACZ,MAAM,CAAC,EAAE,MAAM;aACf,QAAQ,CAAC,EAAE,YAAY;aACvB,OAAO,CAAC,EAAE,kBAAkB;gBAJ5C,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,QAAQ,CAAC,EAAE,YAAY,YAAA,EACvB,OAAO,CAAC,EAAE,kBAAkB,YAAA;CAMnD;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;aAGhB,IAAI,EAAE,MAAM;aACZ,GAAG,CAAC,EAAE,MAAM;gBAF5B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,GAAG,CAAC,EAAE,MAAM,YAAA;CAMnC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/types/errors.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE3E;;GAEG;AACH,qBAAa,SAAU,SAAQ,KAAK;aAWZ,IAAI,EAAE,MAAM;aACZ,MAAM,CAAC,EAAE,MAAM;aACf,QAAQ,CAAC,EAAE,YAAY;aACvB,OAAO,CAAC,EAAE,kBAAkB;aAC5B,SAAS;IAd7B;;;;;;;OAOG;gBAEC,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,MAAM,CAAC,EAAE,MAAM,YAAA,EACf,QAAQ,CAAC,EAAE,YAAY,YAAA,EACvB,OAAO,CAAC,EAAE,kBAAkB,YAAA,EAC5B,SAAS,UAAO;CAMvC;AAED;;;;;;GAMG;AACH,qBAAa,qBAAsB,SAAQ,SAAS;aAO5B,mBAAmB,EAAE,MAAM;IAN/C;;;;OAIG;gBAEiB,mBAAmB,EAAE,MAAM,EAC3C,OAAO,EAAE,kBAAkB,EAC3B,MAAM,CAAC,EAAE,MAAM;CAWtB;AAED;;GAEG;AACH,qBAAa,aAAc,SAAQ,KAAK;aAGhB,IAAI,EAAE,MAAM;aACZ,GAAG,CAAC,EAAE,MAAM;gBAF5B,OAAO,EAAE,MAAM,EACC,IAAI,EAAE,MAAM,EACZ,GAAG,CAAC,EAAE,MAAM,YAAA;CAMnC"}
@@ -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
  export { TraceContext } from './trace.context';
6
7
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,YAAY,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,WAAW,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,iBAAiB,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/utils/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,YAAY,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,WAAW,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,WAAW,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,aAAa,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,iBAAiB,MAAM,iBAAiB,CAAC;AAGrD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { HttpRequestOptions, HttpResponse } from '../types';
2
+ import { EClientAdapter } from '../const';
3
+ /**
4
+ * 判断状态码是否需要跟跳。
5
+ * @param status HTTP 状态码
6
+ */
7
+ export declare function isRedirectStatus(status: number): boolean;
8
+ /**
9
+ * 根据响应里的 Location 构造下一跳请求。
10
+ * 没有 Location 时返回 undefined,调用方把当前 3xx 原样返回。
11
+ * axios:POST 的 301/302,以及非 GET/HEAD 的 303,改成 GET 并去掉 body 和 content-*。
12
+ * undici:只有非 HEAD 的 303 改成 GET 并去掉 body;303 仍去掉 content-*。
13
+ * 两边都去掉 host;跨域去掉 authorization、cookie、proxy-authorization。
14
+ * @param options 当前这一跳的请求
15
+ * @param response 当前这一跳的响应
16
+ * @param adapter 当前适配器
17
+ * @param configuredTimeout 客户端配置中的超时。请求未单独设置时使用。
18
+ */
19
+ export declare function buildRedirectOptions(options: HttpRequestOptions, response: HttpResponse, adapter: EClientAdapter, configuredTimeout?: number): HttpRequestOptions | undefined;
20
+ /**
21
+ * 跟跳共用同一次超时。把本跳已经花掉的时间从 redirectOptions.timeout 扣掉。
22
+ * @param redirectOptions 下一跳请求
23
+ * @param startTime 本跳开始时间
24
+ */
25
+ export declare function consumeRedirectTimeout(redirectOptions: HttpRequestOptions, startTime: number): void;
26
+ //# sourceMappingURL=redirect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redirect.d.ts","sourceRoot":"","sources":["../../../src/utils/redirect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACR,kBAAkB,EAClB,YAAY,EACf,MAAM,UAAU,CAAC;AAGlB,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAe1C;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,cAAc,EACvB,iBAAiB,CAAC,EAAE,MAAM,GAC3B,kBAAkB,GAAG,SAAS,CA6ChC;AAgDD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAClC,eAAe,EAAE,kBAAkB,EACnC,SAAS,EAAE,MAAM,GAClB,IAAI,CAgBN"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nstarter-http-request",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "nstarter HTTP 请求客户端插件",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -7,16 +7,24 @@ import type {
7
7
  HttpResponse,
8
8
  ProxyConfig
9
9
  } from '../types';
10
- import { HttpError } from '../types';
10
+ import {
11
+ HttpError,
12
+ ResponseTooLargeError
13
+ } from '../types';
11
14
  import { BaseHttpAdapter } from './base';
12
15
  import type {
16
+ AxiosProgressEvent,
13
17
  AxiosProxyConfig,
14
18
  AxiosRequestConfig
15
19
  } from 'axios';
16
20
  import axios from 'axios';
17
21
  import { HttpsProxyAgent } from 'https-proxy-agent';
18
22
  import * as https from 'https';
19
- import { EClientAdapter } from '../const';
23
+ import {
24
+ MAX_RESPONSE_SIZE_TOLERANCE_RATIO,
25
+ EClientAdapter
26
+ } from '../const';
27
+ import { RedirectUtils } from '../utils';
20
28
 
21
29
 
22
30
  /**
@@ -42,7 +50,9 @@ export class AxiosAdapter extends BaseHttpAdapter {
42
50
  }
43
51
 
44
52
  const url = this.buildUrl(options.url, options.params);
53
+ const timeout = options.timeout ?? this.requestConfig.timeout;
45
54
  const axiosConfig = this.buildAxiosConfig(options, url);
55
+ const signalState = this.buildAxiosSignals(axiosConfig, options, timeout);
46
56
 
47
57
  try {
48
58
  const startTime = Date.now();
@@ -67,8 +77,80 @@ export class AxiosAdapter extends BaseHttpAdapter {
67
77
 
68
78
  return response;
69
79
  } catch (error: any) {
70
- throw this.handleAxiosError(error, options);
80
+ throw signalState.abortError() ?? this.handleAxiosError(error, options);
81
+ } finally {
82
+ signalState.cleanup();
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 创建整次请求超时与响应体超限两个中止信号,合并后写入 axiosConfig(原地修改)。
88
+ * 返回 catch 中还原中止原因的 abortError(非本方法中止时返回 undefined),以及 finally 中清理定时器的 cleanup。
89
+ */
90
+ private buildAxiosSignals(axiosConfig: AxiosRequestConfig, options: HttpRequestOptions, timeout?: number): {
91
+ abortError: () => HttpError | undefined,
92
+ cleanup: () => void
93
+ } {
94
+ // maxRedirects 为 0 时 axios 使用原生 http 而不是 follow-redirects。
95
+ // 原生 timeout 是套接字空闲超时,在比如持续上传的场景下会被不断重置,需要使用中止信号来结束请求。
96
+ const deadline = this.createDeadline(timeout);
97
+ const bodySizeGuard = this.createBodySizeGuard(options);
98
+ const signals = [deadline.signal, bodySizeGuard.signal].filter((item): item is AbortSignal => !!item);
99
+ axiosConfig.signal = signals.length > 1 ? AbortSignal.any(signals) : signals[0];
100
+ axiosConfig.onDownloadProgress = bodySizeGuard.onDownloadProgress;
101
+
102
+ return {
103
+ abortError: () => {
104
+ if (deadline.timedOut()) {
105
+ return new HttpError(
106
+ `timeout of ${ timeout }ms exceeded`,
107
+ 'ECONNABORTED',
108
+ undefined,
109
+ undefined,
110
+ options
111
+ );
112
+ }
113
+ if (bodySizeGuard.exceeded()) {
114
+ return new ResponseTooLargeError(this.requestConfig.maxResponseBodySize, options);
115
+ }
116
+ return undefined;
117
+ },
118
+ cleanup: deadline.cleanup
119
+ };
120
+ }
121
+
122
+ /**
123
+ * 创建 Content-Length 预检。
124
+ *
125
+ * axios 的 Node http 适配器只按已收到的分块累计 maxContentLength,响应头已声明超限时仍会下载到上限才中断。
126
+ * 这里借助 onDownloadProgress 的 total(响应头 Content-Length),在首个分块到达时就中止请求。
127
+ * - 无 Content-Length(chunked)时不触发,由 axios 的分块累计兜底;
128
+ * - stream 响应不启用,避免额外插入转换流;
129
+ * - total 是线上字节,对几乎不可压缩的 gzip 响应可能略大于解压后的大小,
130
+ * 因此比较时保留 MAX_RESPONSE_SIZE_TOLERANCE_RATIO 的容差。
131
+ */
132
+ private createBodySizeGuard(options: HttpRequestOptions): {
133
+ signal?: AbortSignal,
134
+ onDownloadProgress?: (event: AxiosProgressEvent) => void,
135
+ exceeded: () => boolean
136
+ } {
137
+ const maxRespBodySize = this.resolveMaxResponseBodySize();
138
+ if (options.responseType === 'stream' || maxRespBodySize === undefined) {
139
+ return { exceeded: () => false };
71
140
  }
141
+
142
+ const controller = new AbortController();
143
+ let exceeded = false;
144
+ return {
145
+ signal: controller.signal,
146
+ onDownloadProgress: (event) => {
147
+ if (typeof event.total === 'number' && event.total > maxRespBodySize + Math.ceil(maxRespBodySize * MAX_RESPONSE_SIZE_TOLERANCE_RATIO)) {
148
+ exceeded = true;
149
+ controller.abort();
150
+ }
151
+ },
152
+ exceeded: () => exceeded
153
+ };
72
154
  }
73
155
 
74
156
  /**
@@ -82,6 +164,9 @@ export class AxiosAdapter extends BaseHttpAdapter {
82
164
  url,
83
165
  headers: options.headers || {},
84
166
  maxRedirects: this.requestConfig.maxRedirects,
167
+ validateStatus: (status: number) => {
168
+ return (status >= 200 && status < 300) || status === 304 || RedirectUtils.isRedirectStatus(status);
169
+ },
85
170
  maxContentLength: this.requestConfig.maxResponseBodySize,
86
171
  maxBodyLength: this.requestConfig.maxRequestBodySize,
87
172
  httpsAgent: new https.Agent({
@@ -181,6 +266,12 @@ export class AxiosAdapter extends BaseHttpAdapter {
181
266
  * @param options 请求选项
182
267
  */
183
268
  private handleAxiosError(error: any, options: HttpRequestOptions): HttpError {
269
+ // axios 没有为 maxContentLength 超限提供专用错误码(与 5xx 共用 ERR_BAD_RESPONSE),
270
+ // 只能靠固定的消息前缀识别,识别后转成不可重试的 ResponseTooLargeError。
271
+ if (error.isAxiosError && error.code === 'ERR_BAD_RESPONSE' && error.message.startsWith('maxContentLength size of')) {
272
+ return new ResponseTooLargeError(this.requestConfig.maxResponseBodySize, options);
273
+ }
274
+
184
275
  if (error.isAxiosError) {
185
276
  return new HttpError(
186
277
  error.message,
@@ -226,4 +317,34 @@ export class AxiosAdapter extends BaseHttpAdapter {
226
317
 
227
318
  return config;
228
319
  }
320
+
321
+ /**
322
+ * 按墙钟时间中止请求,到点后 timedOut 为 true
323
+ * @param timeout 超时时间,单位毫秒
324
+ * @returns 中止信号、是否超时、清理函数
325
+ */
326
+ private createDeadline(timeout?: number): {
327
+ signal?: AbortSignal,
328
+ timedOut: () => boolean,
329
+ cleanup: () => void
330
+ } {
331
+ if (typeof timeout !== 'number' || !Number.isFinite(timeout) || timeout <= 0) {
332
+ return {
333
+ timedOut: () => false,
334
+ cleanup: () => undefined
335
+ };
336
+ }
337
+ const controller = new AbortController();
338
+ let timedOut = false;
339
+ const timer = setTimeout(() => {
340
+ timedOut = true;
341
+ controller.abort();
342
+ }, timeout);
343
+ timer.unref?.();
344
+ return {
345
+ signal: controller.signal,
346
+ timedOut: () => timedOut,
347
+ cleanup: () => clearTimeout(timer)
348
+ };
349
+ }
229
350
  }
@@ -10,11 +10,14 @@ import type {
10
10
  SecurityConfig,
11
11
  ILogger
12
12
  } from '../types';
13
+ import { HttpError } from '../types';
13
14
  import { CommonUtils } from '../utils';
14
15
  import type { EClientAdapter } from '../const';
15
16
 
16
17
  /**
17
18
  * HTTP 适配器抽象基类
19
+ *
20
+ * g-7114106599:不允许直接跟跳3XX,防止SSRF。调用方需要自行处理 3XX 响应。
18
21
  */
19
22
  export abstract class BaseHttpAdapter implements IHttpAdapter {
20
23
  protected requestConfig: RequestConfig;
@@ -22,7 +25,7 @@ export abstract class BaseHttpAdapter implements IHttpAdapter {
22
25
  protected logger?: ILogger;
23
26
 
24
27
  constructor(requestConfig: RequestConfig, securityConfig: SecurityConfig, logger?: ILogger) {
25
- this.requestConfig = requestConfig;
28
+ this.requestConfig = { ...requestConfig, maxRedirects: 0 };
26
29
  this.securityConfig = securityConfig;
27
30
  this.logger = logger;
28
31
  }
@@ -85,6 +88,16 @@ export abstract class BaseHttpAdapter implements IHttpAdapter {
85
88
  error: error instanceof Error ? error.message : String(error)
86
89
  });
87
90
 
91
+ // 默认所有错误均可重试;仅在 HttpError 的 retriable 为 false 时终止重试
92
+ if (error instanceof HttpError && !error.retriable) {
93
+ this.logger?.debug('HTTP request error is not retriable, skip remaining retries', {
94
+ method: options.method,
95
+ url: options.url,
96
+ attempt: attempt + 1
97
+ });
98
+ break;
99
+ }
100
+
88
101
  // 最后一次尝试,不再重试
89
102
  if (attempt === retries) {
90
103
  break;
@@ -106,6 +119,14 @@ export abstract class BaseHttpAdapter implements IHttpAdapter {
106
119
  throw lastError;
107
120
  }
108
121
 
122
+ /**
123
+ * @returns 有效的响应体大小上限;不限制时返回 undefined
124
+ */
125
+ protected resolveMaxResponseBodySize(): number | undefined {
126
+ const max = this.requestConfig.maxResponseBodySize;
127
+ return typeof max === 'number' && max > 0 ? max : undefined;
128
+ }
129
+
109
130
  /**
110
131
  * 更新请求配置
111
132
  * @param config 新的请求配置
@@ -4,10 +4,12 @@
4
4
 
5
5
  import type {
6
6
  HttpRequestOptions,
7
- HttpResponse,
8
- ResponseType,
7
+ HttpResponse
8
+ } from '../types';
9
+ import {
10
+ HttpError,
11
+ ResponseTooLargeError
9
12
  } from '../types';
10
- import { HttpError } from '../types';
11
13
  import { BaseHttpAdapter } from './base';
12
14
  import undici, {
13
15
  Agent,
@@ -16,6 +18,7 @@ import undici, {
16
18
  } from 'undici';
17
19
  import { STATUS_CODES } from 'http';
18
20
  import { EClientAdapter } from '../const';
21
+ import { RedirectUtils } from '../utils';
19
22
 
20
23
  interface RequestTimeoutState {
21
24
  signal?: AbortSignal;
@@ -69,7 +72,8 @@ export class UndiciAdapter extends BaseHttpAdapter {
69
72
  timeoutState.cleanup();
70
73
  }
71
74
 
72
- const data = await this.readResponseBody<T>(response, options.responseType);
75
+ // 读取过程中会按 maxResponseBodySize 提前拒绝/中断,超限时不会把完整响应体读入内存
76
+ const data = await this.readResponseBody<T>(response, options);
73
77
 
74
78
  const httpResponse: HttpResponse<T> = {
75
79
  status: response.statusCode,
@@ -79,7 +83,6 @@ export class UndiciAdapter extends BaseHttpAdapter {
79
83
  config: options
80
84
  };
81
85
 
82
- this.ensureResponseBodySize(httpResponse, options.responseType);
83
86
  this.ensureSuccessStatus(httpResponse);
84
87
 
85
88
  return httpResponse;
@@ -96,6 +99,8 @@ export class UndiciAdapter extends BaseHttpAdapter {
96
99
  throw this.handleUndiciError(error, options);
97
100
  } finally {
98
101
  timeoutState.cleanup();
102
+ // buildDispatcher 为每个请求单独创建 Agent/ProxyAgent,用完需要关闭,否则连接池与 keep-alive 连接会一直保留。
103
+ dispatcher?.close().catch(() => undefined);
99
104
  }
100
105
  }
101
106
 
@@ -338,10 +343,14 @@ export class UndiciAdapter extends BaseHttpAdapter {
338
343
  }
339
344
 
340
345
  /**
341
- * 按 axios 默认行为处理非 2xx 状态码。
346
+ * 2xx 和 304 作为成功结果返回。undici 未跟跳的 3xx 交回调用方。
342
347
  */
343
348
  private ensureSuccessStatus(response: HttpResponse): void {
344
- if (response.status >= 200 && response.status < 300) {
349
+ if (
350
+ (response.status >= 200 && response.status < 300)
351
+ || response.status === 304
352
+ || RedirectUtils.isRedirectStatus(response.status)
353
+ ) {
345
354
  return;
346
355
  }
347
356
 
@@ -355,119 +364,122 @@ export class UndiciAdapter extends BaseHttpAdapter {
355
364
  }
356
365
 
357
366
  /**
358
- * 按 axios maxContentLength 行为限制非流式响应体大小。
367
+ * 解析 Content-Length 响应头。仅接受合法的非负整数,重复头/非法值视为未知。
359
368
  */
360
- private ensureResponseBodySize(response: HttpResponse, responseType?: ResponseType): void {
361
- if (responseType === 'stream') {
362
- return;
363
- }
364
- const maxResponseBodySize = this.requestConfig.maxResponseBodySize;
365
- if (
366
- typeof maxResponseBodySize !== 'number' ||
367
- !Number.isFinite(maxResponseBodySize) ||
368
- maxResponseBodySize <= 0
369
- ) {
370
- // NsHttpClient 会先做配置合并和校验;这里防御直接实例化 Adapter 的异常配置。
371
- return;
372
- }
373
- const bodySize = this.getBodySize(response.data);
374
- if (bodySize <= maxResponseBodySize) {
375
- return;
369
+ private parseContentLength(value: string | string[] | undefined): number | undefined {
370
+ if (typeof value !== 'string' || !/^\d+$/.test(value.trim())) {
371
+ return undefined;
376
372
  }
377
- throw new HttpError(
378
- `maxContentLength size of ${ maxResponseBodySize } exceeded`,
379
- 'ERR_BAD_RESPONSE',
380
- response.status,
381
- response,
382
- response.config
383
- );
373
+ const length = Number(value.trim());
374
+ return Number.isSafeInteger(length) ? length : undefined;
384
375
  }
385
376
 
386
377
  /**
387
- * 计算已读取响应体的字节大小。
378
+ * 丢弃未读完的响应体并终止下载。
379
+ * undici 的 BodyReadable.destroy() 不带错误时会以 RequestAbortedError 触发 error 事件,
380
+ * 没有监听器会变成未捕获异常,因此先挂空监听器。
388
381
  */
389
- private getBodySize(body: any): number {
390
- if (body === undefined || body === null) {
391
- return 0;
392
- }
393
- if (Buffer.isBuffer(body)) {
394
- return body.byteLength;
395
- }
396
- if (body instanceof ArrayBuffer) {
397
- return body.byteLength;
398
- }
399
- if (ArrayBuffer.isView(body)) {
400
- return body.byteLength;
382
+ private discardBody(response: Dispatcher.ResponseData): void {
383
+ response.body.on('error', () => undefined);
384
+ response.body.destroy();
385
+ }
386
+
387
+ /**
388
+ * 带大小上限地读取非流式响应体。
389
+ *
390
+ * 1. Content-Length 预检:响应头已声明超限时,不读取响应体,直接销毁并拒绝。
391
+ * 2. 逐分块累计字节数,累计值超过上限(等于上限放行)立即销毁响应流并拒绝,后续数据不再下载;
392
+ * 该行为对齐 axios(Node http 适配器)的 maxContentLength。
393
+ */
394
+ private async readBodyWithLimit(response: Dispatcher.ResponseData, options: HttpRequestOptions): Promise<Buffer> {
395
+ const maxRespBodySize = this.resolveMaxResponseBodySize();
396
+
397
+ // HEAD / 204 / 304 没有响应体,其 Content-Length 描述的是对应实体而不是本次响应体:
398
+ // 不能用于预检;服务端携带时 undici 还会一直等待并不存在的响应体,因此不进入读取,直接丢弃。
399
+ if (options.method === 'HEAD' || response.statusCode === 204 || response.statusCode === 304) {
400
+ this.discardBody(response);
401
+ return Buffer.alloc(0);
401
402
  }
402
- if (typeof Blob !== 'undefined' && body instanceof Blob) {
403
- return body.size;
403
+
404
+ if (maxRespBodySize !== undefined) {
405
+ const contentLength = this.parseContentLength(response.headers['content-length']);
406
+ if (contentLength !== undefined && contentLength > maxRespBodySize) {
407
+ this.discardBody(response);
408
+ throw new ResponseTooLargeError(maxRespBodySize, options, response.statusCode);
409
+ }
404
410
  }
405
- if (typeof body === 'string') {
406
- return Buffer.byteLength(body);
411
+
412
+ const chunks: Buffer[] = [];
413
+ let total = 0;
414
+ for await (const chunk of response.body) {
415
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
416
+ total += buffer.byteLength;
417
+ if (maxRespBodySize !== undefined && total > maxRespBodySize) {
418
+ this.discardBody(response);
419
+ throw new ResponseTooLargeError(maxRespBodySize, options, response.statusCode);
420
+ }
421
+ chunks.push(buffer);
407
422
  }
408
- return Buffer.byteLength(JSON.stringify(body));
423
+ return Buffer.concat(chunks, total);
424
+ }
425
+
426
+ /**
427
+ * 基于已读取(已受大小限制)的字节构造 Response,复用运行时的 blob/formData 解析逻辑。
428
+ */
429
+ private toFetchResponse(buffer: Buffer, response: Dispatcher.ResponseData): Response {
430
+ const contentType = response.headers['content-type'];
431
+ return new Response(buffer, {
432
+ headers: typeof contentType === 'string' ? { 'content-type': contentType } : undefined
433
+ });
409
434
  }
410
435
 
411
436
  /**
412
437
  * 读取响应体
413
438
  * @param response Undici 响应对象
414
- * @param responseType 响应类型
439
+ * @param options 请求选项
415
440
  */
416
- private async readResponseBody<T>(response: Dispatcher.ResponseData, responseType?: ResponseType): Promise<T> {
441
+ private async readResponseBody<T>(response: Dispatcher.ResponseData, options: HttpRequestOptions): Promise<T> {
442
+ if (options.responseType === 'stream') {
443
+ // undici 返回 BodyReadable (继承自 Node.js Readable),流式响应由调用方自行消费,不受 maxResponseBodySize 限制
444
+ return response.body as T;
445
+ }
446
+
417
447
  try {
418
- switch (responseType) {
448
+ const buffer = await this.readBodyWithLimit(response, options);
449
+ const decoder = new TextDecoder();
450
+ switch (options.responseType) {
419
451
  case 'text': {
420
- return (await response.body.text()) as T;
452
+ return decoder.decode(buffer) as T;
421
453
  }
422
454
  case 'arraybuffer': {
423
- return Buffer.from(await response.body.arrayBuffer()) as T;
424
- }
425
- case 'stream': {
426
- // undici 返回 BodyReadable (继承自 Node.js Readable)
427
- return response.body as T;
455
+ return buffer as T;
428
456
  }
429
457
  case 'blob': {
430
- return (await response.body.blob()) as T;
458
+ return (await this.toFetchResponse(buffer, response).blob()) as T;
431
459
  }
432
460
  case 'formdata': {
433
- return (await response.body.formData()) as T;
434
- }
435
- case 'json': {
436
- return await this.readJsonResponseBody<T>(response);
461
+ return (await this.toFetchResponse(buffer, response).formData()) as T;
437
462
  }
463
+ case 'json':
438
464
  default: {
439
- const text = await response.body.text();
465
+ // 按 axios 的默认兼容行为:空响应体或非 JSON 文本不应让成功状态码请求因为解析失败变成异常
466
+ const text = decoder.decode(buffer);
440
467
  try {
441
468
  return JSON.parse(text) as T;
442
469
  } catch {
443
- // 如果不是有效的 JSON,返回原文本
444
470
  return text as T;
445
471
  }
446
472
  }
447
-
448
473
  }
449
474
  } catch (error) {
475
+ // 响应体超限错误需要原样抛出,保留错误码、文案与不可重试标记
476
+ if (error instanceof ResponseTooLargeError) {
477
+ throw error;
478
+ }
450
479
  throw new Error(`Failed to read response body: ${ error instanceof Error ? error.message : String(error) }`);
451
480
  }
452
481
  }
453
482
 
454
- /**
455
- * 按 axios 的默认兼容行为读取 JSON 响应。
456
- * 空响应体或非 JSON 文本不应让成功状态码请求因为解析失败变成异常。
457
- */
458
- private async readJsonResponseBody<T>(response: Dispatcher.ResponseData): Promise<T> {
459
- const text = await response.body.text();
460
- if (!text) {
461
- return text as T;
462
- }
463
-
464
- try {
465
- return JSON.parse(text) as T;
466
- } catch {
467
- return text as T;
468
- }
469
- }
470
-
471
483
  /**
472
484
  * 获取状态文本
473
485
  * @param statusCode 状态码