@maiyunnet/kebab 9.15.6 → 9.16.1

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/index.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * --- 本文件用来定义每个目录实体地址的常量 ---
6
6
  */
7
7
  /** --- 当前系统版本号 --- */
8
- export declare const VER = "9.15.6";
8
+ export declare const VER = "9.16.1";
9
9
  /** --- 框架根目录,以 / 结尾 --- */
10
10
  export declare const ROOT_PATH: string;
11
11
  /** --- 框架的 LIB,以 / 结尾 --- */
package/index.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * --- 本文件用来定义每个目录实体地址的常量 ---
7
7
  */
8
8
  /** --- 当前系统版本号 --- */
9
- export const VER = '9.15.6';
9
+ export const VER = '9.16.1';
10
10
  // --- 服务端用的路径 ---
11
11
  const imu = decodeURIComponent(import.meta.url).replace('file://', '').replace(/^\/(\w:)/, '$1');
12
12
  /** --- /xxx/xxx --- */
package/lib/core.d.ts CHANGED
@@ -352,12 +352,31 @@ export declare function debug(message?: any, ...optionalParams: any[]): void;
352
352
  */
353
353
  export declare function display(message?: any, ...optionalParams: any[]): void;
354
354
  /**
355
- * --- res 发送头部(前提是头部没有被发送才能调用本方法 ---
355
+ * --- 提交 HTTP 响应状态和头部,兼容 HTTP/1.1 与 HTTP/2 ---
356
+ *
357
+ * `setHeader()` 只暂存或修改单个头部,不会发送响应头,也不能提交状态码;在响应头提交前可反复调用。
358
+ * 本方法会立即提交状态码和此前设置的全部头部,必须在所有 `setHeader()` 调用之后、首次
359
+ * `write()`、`end()` 或 `pipe()` 之前调用。提交后再调用 `setHeader()` 会抛出 `ERR_HTTP_HEADERS_SENT`。
360
+ *
361
+ * 普通控制器应设置 `_httpCode`、调用 `_res.setHeader()` 并直接返回内容,由路由层统一提交响应头。
362
+ * 仅框架内部或手动接管响应(错误、重定向、代理、流式输出等)时才应直接调用本方法。
363
+ *
356
364
  * @param res 响应对象
357
- * @param statusCode 状态码
358
- * @param headers 头部
365
+ * @param statusCode HTTP 状态码
366
+ * @param headers 随本次提交附加的头部;通常优先在提交前使用 `setHeader()`
367
+ * @returns 无返回值
359
368
  */
360
369
  export declare function writeHead(res: http2.Http2ServerResponse | http.ServerResponse, statusCode: number, headers?: http.OutgoingHttpHeaders): void;
370
+ /**
371
+ * --- 提交服务器发送事件(SSE)响应头 ---
372
+ *
373
+ * 在发送第一条事件前调用一次,固定提交状态码 200、事件流内容类型和禁止缓存头部。
374
+ * SSE 的内容长度和结束时间未知,因此不设置 `content-length`;调用后响应头已经提交,
375
+ * 不能再调用 `setHeader()`,后续应使用 `write()` 持续发送事件,并由调用方在结束时关闭响应。
376
+ *
377
+ * @param res 响应对象
378
+ * @returns 无返回值
379
+ */
361
380
  export declare function writeEventStreamHead(res: http2.Http2ServerResponse | http.ServerResponse): void;
362
381
  /**
363
382
  * --- 向 res 发送数据 ---
package/lib/core.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2019-5-3 23:54
4
- * Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53, 2026-05-20 09:50:00
4
+ * Last: 2020-4-11 22:34:58, 2022-10-2 14:13:06, 2022-12-28 20:33:24, 2023-12-15 11:49:02, 2024-7-2 15:23:35, 2025-6-13 19:45:53, 2026-05-20 09:50:00, 2026-8-22
5
5
  */
6
6
  import * as cp from 'child_process';
7
7
  import * as http2 from 'http2';
@@ -1221,10 +1221,19 @@ export function display(message, ...optionalParams) {
1221
1221
  console.log(`KE-DISPLAY ${lTime.format(null, 'Y-m-d H:i:s')}`, message, ...optionalParams);
1222
1222
  }
1223
1223
  /**
1224
- * --- res 发送头部(前提是头部没有被发送才能调用本方法 ---
1224
+ * --- 提交 HTTP 响应状态和头部,兼容 HTTP/1.1 与 HTTP/2 ---
1225
+ *
1226
+ * `setHeader()` 只暂存或修改单个头部,不会发送响应头,也不能提交状态码;在响应头提交前可反复调用。
1227
+ * 本方法会立即提交状态码和此前设置的全部头部,必须在所有 `setHeader()` 调用之后、首次
1228
+ * `write()`、`end()` 或 `pipe()` 之前调用。提交后再调用 `setHeader()` 会抛出 `ERR_HTTP_HEADERS_SENT`。
1229
+ *
1230
+ * 普通控制器应设置 `_httpCode`、调用 `_res.setHeader()` 并直接返回内容,由路由层统一提交响应头。
1231
+ * 仅框架内部或手动接管响应(错误、重定向、代理、流式输出等)时才应直接调用本方法。
1232
+ *
1225
1233
  * @param res 响应对象
1226
- * @param statusCode 状态码
1227
- * @param headers 头部
1234
+ * @param statusCode HTTP 状态码
1235
+ * @param headers 随本次提交附加的头部;通常优先在提交前使用 `setHeader()`
1236
+ * @returns 无返回值
1228
1237
  */
1229
1238
  export function writeHead(res, statusCode, headers) {
1230
1239
  if (res instanceof http2.Http2ServerResponse) {
@@ -1234,6 +1243,16 @@ export function writeHead(res, statusCode, headers) {
1234
1243
  res.writeHead(statusCode, headers);
1235
1244
  }
1236
1245
  }
1246
+ /**
1247
+ * --- 提交服务器发送事件(SSE)响应头 ---
1248
+ *
1249
+ * 在发送第一条事件前调用一次,固定提交状态码 200、事件流内容类型和禁止缓存头部。
1250
+ * SSE 的内容长度和结束时间未知,因此不设置 `content-length`;调用后响应头已经提交,
1251
+ * 不能再调用 `setHeader()`,后续应使用 `write()` 持续发送事件,并由调用方在结束时关闭响应。
1252
+ *
1253
+ * @param res 响应对象
1254
+ * @returns 无返回值
1255
+ */
1237
1256
  export function writeEventStreamHead(res) {
1238
1257
  writeHead(res, 200, {
1239
1258
  'content-type': 'text/event-stream; charset=utf-8',
package/lib/db/conn.js CHANGED
@@ -3,6 +3,7 @@ import * as lCore from '#kebab/lib/core.js';
3
3
  import * as lTime from '#kebab/lib/time.js';
4
4
  import * as lDb from '#kebab/lib/db.js';
5
5
  import * as lSqlValue from '#kebab/lib/sql/value.js';
6
+ import * as lText from '#kebab/lib/text.js';
6
7
  // --- 注册解析器 ---
7
8
  // --- pg 库对以下类型默认返回 string,为与 MySQL 行为保持一致,在此注册解析器转为 JS 原生类型 ---
8
9
  // --- POLYGON: 返回如 ((1,1),(2,2)) 的字符串,解析为 {x, y}[] ---
@@ -301,11 +302,16 @@ export class Connection {
301
302
  }
302
303
  return true;
303
304
  }
304
- catch {
305
+ catch (e) {
306
+ this._transaction = false;
307
+ this._using = false;
308
+ this._lost = true;
309
+ lCore.log({}, '[DB][Connection][beginTransaction] ' + lText.stringifyError(e), '-error');
305
310
  return false;
306
311
  }
307
312
  }
308
313
  else {
314
+ lCore.log({}, '[DB][Connection][beginTransaction] connection is not in use', '-error');
309
315
  return false;
310
316
  }
311
317
  }
@@ -322,7 +328,9 @@ export class Connection {
322
328
  this._using = false;
323
329
  return true;
324
330
  }
325
- catch {
331
+ catch (e) {
332
+ this._lost = true;
333
+ lCore.log({}, '[DB][Connection][commit] ' + lText.stringifyError(e), '-error');
326
334
  return false;
327
335
  }
328
336
  }
@@ -339,7 +347,9 @@ export class Connection {
339
347
  this._using = false;
340
348
  return true;
341
349
  }
342
- catch {
350
+ catch (e) {
351
+ this._lost = true;
352
+ lCore.log({}, '[DB][Connection][rollback] ' + lText.stringifyError(e), '-error');
343
353
  return false;
344
354
  }
345
355
  }
package/lib/db/pool.js CHANGED
@@ -8,6 +8,10 @@ import { Connection } from './conn.js';
8
8
  import { Transaction } from './tran.js';
9
9
  /** --- 连接列表池 --- */
10
10
  const connections = [];
11
+ /** --- 开启事务最大尝试次数:首次尝试加一次换连接重试 --- */
12
+ const BEGIN_TRANSACTION_MAX_ATTEMPTS = 2;
13
+ /** --- 创建连接最大尝试次数:首次尝试加两次瞬时故障重试 --- */
14
+ const CREATE_CONNECTION_MAX_ATTEMPTS = 3;
11
15
  /**
12
16
  * --- 获取当前连接池中所有连接的信息 ---
13
17
  */
@@ -157,14 +161,19 @@ export class Pool {
157
161
  * --- 开启事务,返回事务对象并锁定连接,别人任何人不可用,有 ctr 的话必传 this,独立执行时可传 null ---
158
162
  */
159
163
  async beginTransaction(ctr) {
160
- const conn = await this._getConnection();
161
- if (!conn) {
162
- return null;
163
- }
164
- if (!await conn.beginTransaction()) {
165
- return null;
164
+ for (let i = 0; i < BEGIN_TRANSACTION_MAX_ATTEMPTS; ++i) {
165
+ const conn = await this._getConnection();
166
+ if (!conn) {
167
+ lCore.log(ctr ?? {}, `[DB][Pool][beginTransaction] failed to get connection, service: ${lDb.ESERVICE[this._service]}, database: ${this._etc.name ?? ''}`, '-error');
168
+ return null;
169
+ }
170
+ if (!await conn.beginTransaction()) {
171
+ continue;
172
+ }
173
+ return new Transaction(ctr, conn);
166
174
  }
167
- return new Transaction(ctr, conn);
175
+ lCore.log(ctr ?? {}, `[DB][Pool][beginTransaction] failed after retry, service: ${lDb.ESERVICE[this._service]}, database: ${this._etc.name ?? ''}`, '-error');
176
+ return null;
168
177
  }
169
178
  /**
170
179
  * --- 获取一个连接,自动变为 using 状态,;连接失败会返回 null ---
@@ -192,7 +201,7 @@ export class Pool {
192
201
  }
193
202
  if (!conn) {
194
203
  // --- 没有找到合适的连接,创建一个 ---
195
- loop: for (let i = 0; i < 3; ++i) {
204
+ loop: for (let i = 0; i < CREATE_CONNECTION_MAX_ATTEMPTS; ++i) {
196
205
  try {
197
206
  switch (this._service) {
198
207
  case lDb.ESERVICE.MYSQL: {
@@ -267,12 +276,13 @@ export class Pool {
267
276
  }
268
277
  }
269
278
  catch (err) {
270
- if (err.message.includes('ETIMEOUT') || err.message.includes('EHOSTUNREACH') || err.message.includes('ECONNREFUSED')) {
271
- // lCore.debug(`[DB][_getConnection][${lDb.ESERVICE[this._service]}]`, err);
279
+ const message = err instanceof Error ? err.message : String(err);
280
+ const transient = ['ETIMEOUT', 'EHOSTUNREACH', 'ECONNREFUSED'].some(code => message.includes(code));
281
+ if (transient && i < CREATE_CONNECTION_MAX_ATTEMPTS - 1) {
272
282
  await lCore.sleep(300);
273
283
  continue;
274
284
  }
275
- const msg = `[DB][_getConnection][${lDb.ESERVICE[this._service]}] ${err.message}(${this._etc.host}:${this._etc.port})`;
285
+ const msg = `[DB][_getConnection][${lDb.ESERVICE[this._service]}] ${message}(${this._etc.host}:${this._etc.port})`;
276
286
  lCore.debug(msg);
277
287
  lCore.log({}, msg, '-error');
278
288
  break;
package/lib/text.d.ts CHANGED
@@ -170,6 +170,12 @@ export declare function parseJson<T>(str: string): T | false;
170
170
  * @param space 美化方式
171
171
  */
172
172
  export declare function stringifyJson(obj: kebab.Json, space?: string | number): string;
173
+ /**
174
+ * --- 将未知异常转换为适合单行日志的文本,Error 优先返回完整堆栈 ---
175
+ * @param error 异常对象
176
+ * @returns 已转义的单行错误文本
177
+ */
178
+ export declare function stringifyError(error: unknown): string;
173
179
  /**
174
180
  * --- 输出文本格式的 buffer ---
175
181
  * @param buf 原始 buffer
package/lib/text.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2019-5-15 16:49:39
4
- * Last: 2020-04-06 20:51:06, 2022-9-29 15:18:16, 2022-12-29 00:01:30, 2024-3-6 17:53:14, 2024-5-31 17:29:52, 2025-6-13 15:47:02, 2025-9-23 12:51:49
4
+ * Last: 2020-04-06 20:51:06, 2022-9-29 15:18:16, 2022-12-29 00:01:30, 2024-3-6 17:53:14, 2024-5-31 17:29:52, 2025-6-13 15:47:02, 2025-9-23 12:51:49, 2026-8-22
5
5
  */
6
6
  import * as net from 'net';
7
7
  import * as kebab from '#kebab/index.js';
@@ -624,6 +624,29 @@ export function stringifyJson(obj, space) {
624
624
  return v;
625
625
  }, space).replace(/"-mybigint-([-+0-9]+?)"/g, '$1');
626
626
  }
627
+ /**
628
+ * --- 将未知异常转换为适合单行日志的文本,Error 优先返回完整堆栈 ---
629
+ * @param error 异常对象
630
+ * @returns 已转义的单行错误文本
631
+ */
632
+ export function stringifyError(error) {
633
+ let message;
634
+ if (error instanceof Error) {
635
+ message = error.stack ?? error.message;
636
+ }
637
+ else if (typeof error === 'string') {
638
+ message = error;
639
+ }
640
+ else {
641
+ try {
642
+ message = stringifyJson(error);
643
+ }
644
+ catch {
645
+ message = String(error);
646
+ }
647
+ }
648
+ return stringifyJson(message).slice(1, -1);
649
+ }
627
650
  /**
628
651
  * --- 输出文本格式的 buffer ---
629
652
  * @param buf 原始 buffer
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2020-4-9 20:02:39
4
- * Last: 2020-4-9 20:47:58, 2022-09-10 01:35:34, 2025-9-23 12:41:58
4
+ * Last: 2020-4-9 20:47:58, 2022-09-10 01:35:34, 2025-9-23 12:41:58, 2026-8-22
5
5
  */
6
6
  import * as stream from 'stream';
7
7
  import * as lCookie from '#kebab/lib/cookie.js';
@@ -47,6 +47,11 @@ export declare class Request {
47
47
  * @param timeout 秒
48
48
  */
49
49
  timeout(timeout: number): this;
50
+ /**
51
+ * --- 设置网络异常后的重试次数 ---
52
+ * @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
53
+ */
54
+ retry(retry?: number): this;
50
55
  /**
51
56
  * --- 设置是否跟随请求方的 location,留空为跟随,不设置为不跟随 ---
52
57
  * @param follow
@@ -59,6 +59,14 @@ export class Request {
59
59
  this._opt['timeout'] = timeout;
60
60
  return this;
61
61
  }
62
+ /**
63
+ * --- 设置网络异常后的重试次数 ---
64
+ * @param retry 重试次数,默认为 1;非幂等请求需由调用方保证安全
65
+ */
66
+ retry(retry = 1) {
67
+ this._opt['retry'] = retry;
68
+ return this;
69
+ }
62
70
  /**
63
71
  * --- 设置是否跟随请求方的 location,留空为跟随,不设置为不跟随 ---
64
72
  * @param follow
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2020-4-9 15:33:06
4
- * Last: 2020-4-12 11:12:03, 2022-09-10 12:43:23, 2022-12-25 15:12:57, 2023-9-26 14:20:41
4
+ * Last: 2020-4-12 11:12:03, 2022-09-10 12:43:23, 2022-12-25 15:12:57, 2023-9-26 14:20:41, 2026-8-22
5
5
  */
6
6
  import * as undici from 'undici';
7
7
  import * as zlib from 'zlib';
@@ -31,7 +31,7 @@ export declare class Response {
31
31
  /**
32
32
  * --- 获取响应读取流对象 ---
33
33
  */
34
- getStream(): (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | zlib.Gunzip | zlib.Inflate | zlib.BrotliDecompress | null;
34
+ getStream(): zlib.BrotliDecompress | zlib.Gunzip | zlib.Inflate | (import("undici/types/readable").default & undici.Dispatcher.BodyMixin) | null;
35
35
  /**
36
36
  * --- 获取原生响应读取流对象 ---
37
37
  */
@@ -28,7 +28,8 @@ export class Response {
28
28
  return this._req ? await lBuffer.getFull(stream) : null;
29
29
  }
30
30
  catch (e) {
31
- lCore.log({}, '[Undici][Response][getContent] ' + e.message, '-error');
31
+ this.error = e instanceof Error ? e : new Error(String(e));
32
+ lCore.log({}, '[Undici][Response][getContent] ' + lText.stringifyError(e), '-error');
32
33
  return null;
33
34
  }
34
35
  }
package/lib/undici.d.ts CHANGED
@@ -36,27 +36,28 @@ export declare function post(u: string, data: Record<string, kebab.Json> | Buffe
36
36
  */
37
37
  export declare function postJson(u: string, data: kebab.Json[] | Record<string, kebab.Json>, opt?: IRequestOptions): Promise<lResponse.Response>;
38
38
  /**
39
- * --- 发起 JSON 请求并解析 JSON 响应,失败时返回 null ---
39
+ * --- 发起 JSON 请求并解析 JSON 响应 ---
40
40
  * @param u 网址
41
41
  * @param data 数据
42
42
  * @param opt 选项
43
+ * @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
43
44
  */
44
- export declare function postJsonResponseJson(u: string, data: kebab.Json[] | Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null>;
45
+ export declare function postJsonResponseJson(u: string, data: kebab.Json[] | Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
45
46
  /**
46
47
  * --- 发起 POST 请求并解析 JSON 响应 ---
47
48
  * @param u 网址
48
49
  * @param data 数据
49
50
  * @param opt 选项
50
- * @returns JSON 数据,失败时返回 null
51
+ * @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
51
52
  */
52
- export declare function postResponseJson(u: string, data: Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null>;
53
+ export declare function postResponseJson(u: string, data: Record<string, kebab.Json>, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
53
54
  /**
54
55
  * --- 发起 GET 请求并解析 JSON 响应 ---
55
56
  * @param u 网址
56
57
  * @param opt 选项
57
- * @returns JSON 数据,失败时返回 null
58
+ * @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
58
59
  */
59
- export declare function getResponseJson(u: string, opt?: IRequestOptions): Promise<kebab.Json | null>;
60
+ export declare function getResponseJson(u: string, opt?: IRequestOptions): Promise<kebab.Json | null | false>;
60
61
  /**
61
62
  * --- 发起一个完全兼容 fetch 的请求 ---
62
63
  * @param input 请求的 URL 或 Request 对象
@@ -71,6 +72,8 @@ export declare function fetch(input: string | URL | Request, init?: RequestInit
71
72
  };
72
73
  /** --- 自定义 host 映射,如 {'www.maiyun.net': '127.0.0.1'},或全部映射到一个 host --- */
73
74
  'hosts'?: Record<string, string> | string;
75
+ /** --- 网络异常后的重试次数,默认 0;流式请求体不可重试,非幂等请求需由调用方保证安全 --- */
76
+ 'retry'?: number;
74
77
  }): Promise<Response>;
75
78
  /**
76
79
  * --- 发起一个请求 ---
@@ -126,6 +129,10 @@ export interface IRequestOptions {
126
129
  'type'?: 'form' | 'json';
127
130
  /** --- 秒数,默认 300 秒 --- */
128
131
  'timeout'?: number;
132
+ /** --- 网络异常后的重试次数,默认 0;流式请求体不可重试,非幂等请求需由调用方保证安全 --- */
133
+ 'retry'?: number;
134
+ /** --- JSON 解析失败后的重试次数,默认 0;仅适用于 ResponseJson 快捷方法,非幂等请求需由调用方保证安全 --- */
135
+ 'retryJson'?: number;
129
136
  /** --- 追踪 location 次数,0 为不追踪,默认为 0 --- */
130
137
  'follow'?: number;
131
138
  /** --- 自定义 host 映射,如 {'www.maiyun.net': '127.0.0.1'},或全部映射到一个 host --- */
package/lib/undici.js CHANGED
@@ -26,6 +26,25 @@ function getMproxyUrl(u, mproxy) {
26
26
  }
27
27
  /** --- 复用的 undici.agent 对象列表 --- */
28
28
  const agents = new Map();
29
+ /** --- 可重试的网络异常代码 --- */
30
+ const retryErrorCodes = [
31
+ 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN', 'ENETDOWN', 'ENETUNREACH',
32
+ 'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'ETIMEDOUT', 'UND_ERR_SOCKET',
33
+ 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT'
34
+ ];
35
+ /** --- JSON 解析重试的初始等待时间,单位毫秒 --- */
36
+ const RETRY_JSON_MIN_TIMEOUT = 500;
37
+ /** --- JSON 解析重试的最大等待时间,单位毫秒 --- */
38
+ const RETRY_JSON_MAX_TIMEOUT = 30_000;
39
+ /**
40
+ * --- 将重试次数规范为非负整数 ---
41
+ * @param retry 重试次数
42
+ * @returns 可执行的重试次数
43
+ */
44
+ function normalizeRetry(retry) {
45
+ return (typeof retry === 'number') && Number.isFinite(retry) ?
46
+ Math.max(0, Math.floor(retry)) : 0;
47
+ }
29
48
  /** --- 获取或创建 undici.agent 对象 --- */
30
49
  function getAgent(opt = {}) {
31
50
  let k = opt.reuse ?? 'default';
@@ -59,6 +78,26 @@ function getAgent(opt = {}) {
59
78
  }
60
79
  return agents.get(k);
61
80
  }
81
+ /**
82
+ * --- 获取请求使用的 dispatcher ---
83
+ * @param opt 请求选项
84
+ * @param method 请求方法
85
+ * @param data 请求数据
86
+ * @returns 普通或带网络异常重试能力的 dispatcher
87
+ */
88
+ function getDispatcher(opt, method, data) {
89
+ const agent = getAgent(opt);
90
+ const retry = normalizeRetry(opt.retry);
91
+ if ((retry === 0) || (data instanceof stream.Readable)) {
92
+ return agent;
93
+ }
94
+ return new undici.RetryAgent(agent, {
95
+ 'maxRetries': retry,
96
+ 'methods': [method],
97
+ 'statusCodes': [],
98
+ 'errorCodes': retryErrorCodes,
99
+ });
100
+ }
62
101
  /**
63
102
  * --- 创建一个请求对象 ---
64
103
  * @param u
@@ -96,62 +135,70 @@ export async function postJson(u, data, opt = {}) {
96
135
  return request(u, data, opt);
97
136
  }
98
137
  /**
99
- * --- 发起 JSON 请求并解析 JSON 响应,失败时返回 null ---
138
+ * --- 发起请求并解析 JSON 响应,可在解析失败时重新请求 ---
139
+ * @param u 网址
140
+ * @param data 请求数据
141
+ * @param opt 请求选项
142
+ * @param action 日志动作名
143
+ * @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
144
+ */
145
+ async function requestResponseJson(u, data, opt, action) {
146
+ const retryJson = normalizeRetry(opt.retryJson);
147
+ for (let i = 0; i <= retryJson; ++i) {
148
+ const res = await request(u, data, opt);
149
+ const rtn = await res.getContent();
150
+ if (!rtn) {
151
+ return null;
152
+ }
153
+ const rtnStr = rtn.toString();
154
+ const json = lText.parseJson(rtnStr);
155
+ if (json) {
156
+ return json;
157
+ }
158
+ if (i < retryJson) {
159
+ const timeout = Math.min(RETRY_JSON_MIN_TIMEOUT * (2 ** i), RETRY_JSON_MAX_TIMEOUT);
160
+ await lCore.sleep(timeout);
161
+ continue;
162
+ }
163
+ if (opt.log === undefined || opt.log) {
164
+ const requestData = data === undefined ? '' : `, data: ${lText.stringifyJson(data)}`;
165
+ lCore.log({}, `[UNDICI][${action}] parse json failed, url: ${u}${requestData}, content: ${rtnStr}`, '-neterror');
166
+ }
167
+ return false;
168
+ }
169
+ return false;
170
+ }
171
+ /**
172
+ * --- 发起 JSON 请求并解析 JSON 响应 ---
100
173
  * @param u 网址
101
174
  * @param data 数据
102
175
  * @param opt 选项
176
+ * @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
103
177
  */
104
178
  export async function postJsonResponseJson(u, data, opt = {}) {
105
179
  opt.method = 'POST';
106
180
  opt.type = 'json';
107
- const res = await request(u, data, opt);
108
- const rtn = await res.getContent();
109
- if (!rtn) {
110
- return null;
111
- }
112
- const json = lText.parseJson(rtn.toString());
113
- if (!json) {
114
- return null;
115
- }
116
- return json;
181
+ return requestResponseJson(u, data, opt, 'POSTJSONRESPONSEJSON');
117
182
  }
118
183
  /**
119
184
  * --- 发起 POST 请求并解析 JSON 响应 ---
120
185
  * @param u 网址
121
186
  * @param data 数据
122
187
  * @param opt 选项
123
- * @returns JSON 数据,失败时返回 null
188
+ * @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
124
189
  */
125
190
  export async function postResponseJson(u, data, opt = {}) {
126
191
  opt.method = 'POST';
127
- const res = await request(u, data, opt);
128
- const rtn = await res.getContent();
129
- if (!rtn) {
130
- return null;
131
- }
132
- const json = lText.parseJson(rtn.toString());
133
- if (!json) {
134
- return null;
135
- }
136
- return json;
192
+ return requestResponseJson(u, data, opt, 'POSTRESPONSEJSON');
137
193
  }
138
194
  /**
139
195
  * --- 发起 GET 请求并解析 JSON 响应 ---
140
196
  * @param u 网址
141
197
  * @param opt 选项
142
- * @returns JSON 数据,失败时返回 null
198
+ * @returns JSON 数据;请求失败返回 null;JSON 解析失败返回 false
143
199
  */
144
200
  export async function getResponseJson(u, opt = {}) {
145
- const res = await request(u, undefined, opt);
146
- const rtn = await res.getContent();
147
- if (!rtn) {
148
- return null;
149
- }
150
- const json = lText.parseJson(rtn.toString());
151
- if (!json) {
152
- return null;
153
- }
154
- return json;
201
+ return requestResponseJson(u, undefined, opt, 'GETRESPONSEJSON');
155
202
  }
156
203
  /**
157
204
  * --- 发起一个完全兼容 fetch 的请求 ---
@@ -244,6 +291,7 @@ export async function fetch(input, init = {}) {
244
291
  'headers': headers,
245
292
  'hosts': init.hosts,
246
293
  'mproxy': init.mproxy,
294
+ 'retry': init.retry,
247
295
  'signal': init.signal ?? undefined,
248
296
  'follow': init.redirect === 'follow' ? 10 : 0,
249
297
  };
@@ -387,7 +435,7 @@ export async function request(u, data, opt = {}) {
387
435
  }
388
436
  return res;
389
437
  }
390
- const agent = getAgent(opt);
438
+ const dispatcher = getDispatcher(opt, method, data);
391
439
  req = await undici.request(opt.mproxy ? getMproxyUrl(u, opt.mproxy) : u, {
392
440
  'method': method,
393
441
  'body': data,
@@ -395,7 +443,7 @@ export async function request(u, data, opt = {}) {
395
443
  'headersTimeout': timeout * 1_000,
396
444
  'bodyTimeout': timeout * 1_000,
397
445
  'signal': opt.signal,
398
- 'dispatcher': agent,
446
+ 'dispatcher': dispatcher,
399
447
  });
400
448
  }
401
449
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maiyunnet/kebab",
3
- "version": "9.15.6",
3
+ "version": "9.16.1",
4
4
  "description": "Simple, easy-to-use, and fully-featured Node.js framework that is ready-to-use out of the box.",
5
5
  "type": "module",
6
6
  "keywords": [
package/sys/child.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Project: Kebab, User: JianSuoQiYue
3
3
  * Date: 2019-5-3 23:54
4
- * Last: 2020-3-31 15:01:07, 2020-4-9 22:28:50, 2022-07-22 14:19:46, 2022-9-29 22:11:07, 2023-5-1 18:26:57, 2024-1-12 13:32:00, 2024-3-4 16:49:19, 2026-3-8 16:21:40
4
+ * Last: 2020-3-31 15:01:07, 2020-4-9 22:28:50, 2022-07-22 14:19:46, 2022-9-29 22:11:07, 2023-5-1 18:26:57, 2024-1-12 13:32:00, 2024-3-4 16:49:19, 2026-3-8 16:21:40, 2026-8-22
5
5
  */
6
6
  import * as http2 from 'http2';
7
7
  import * as tls from 'tls';
@@ -57,15 +57,18 @@ const http2Sessions = new Set();
57
57
  * @param method 请求方法
58
58
  */
59
59
  function wrapWithLinkCount(key, handler, errorPrefix, method = 'GET') {
60
- linkCount[key] = (linkCount[key] ?? 0) + 1;
61
- const trackId = sMonitor.track(key, method);
62
- handler().catch((e) => {
63
- lCore.log({}, `${errorPrefix} ${lText.stringifyJson(e.stack).slice(1, -1)}`, '-error');
60
+ const queryIndex = key.search(/[?#]/u);
61
+ /** --- 查询参数可能含敏感信息,请求计数与诊断仅保留路径 --- */
62
+ const requestKey = queryIndex === -1 ? key : key.slice(0, queryIndex);
63
+ linkCount[requestKey] = (linkCount[requestKey] ?? 0) + 1;
64
+ const trackId = sMonitor.track(requestKey, method);
65
+ Promise.resolve().then(handler).catch((e) => {
66
+ lCore.log({}, `${errorPrefix} ${lText.stringifyError(e)}`, '-error');
64
67
  }).finally(() => {
65
68
  sMonitor.untrack(trackId);
66
- --linkCount[key];
67
- if (!linkCount[key]) {
68
- delete linkCount[key];
69
+ --linkCount[requestKey];
70
+ if (!linkCount[requestKey]) {
71
+ delete linkCount[requestKey];
69
72
  }
70
73
  });
71
74
  }
@@ -295,8 +298,8 @@ async function requestHandler(req, res, https) {
295
298
  'req': req,
296
299
  'get': uri.query ? lText.queryParse(uri.query) : {},
297
300
  'cookie': {},
298
- 'headers': {}
299
- }, '[CHILD][requestHandler][E0]' + lText.stringifyJson(e.stack).slice(1, -1), '-error');
301
+ 'headers': {},
302
+ }, '[CHILD][requestHandler][E0]' + lText.stringifyError(e), '-error');
300
303
  const content = '<h1>500 Server Error</h1><hr>Kebab';
301
304
  if (!res.headersSent) {
302
305
  res.setHeader('content-type', 'text/html; charset=utf-8');
@@ -339,7 +342,7 @@ async function requestHandler(req, res, https) {
339
342
  }
340
343
  }
341
344
  catch (e) {
342
- lCore.log({}, '[CHILD][requestHandler][E1]' + lText.stringifyJson(e.stack).slice(1, -1), '-error');
345
+ lCore.log({}, '[CHILD][requestHandler][E1]' + lText.stringifyError(e), '-error');
343
346
  const content = '<h1>500 Server Error</h1><hr>Kebab';
344
347
  if (!res.headersSent) {
345
348
  res.setHeader('content-type', 'text/html; charset=utf-8');
@@ -618,7 +621,7 @@ process.on('message', function (msg) {
618
621
  }
619
622
  }
620
623
  })().catch(function (e) {
621
- lCore.log({}, '[CHILD][process][message] ' + lText.stringifyJson(e.stack).slice(1, -1), '-error');
624
+ lCore.log({}, '[CHILD][process][message] ' + lText.stringifyError(e), '-error');
622
625
  });
623
626
  });
624
627
  /**