niceeval 0.14.1-canary.51 → 0.14.2

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 (46) hide show
  1. package/INDEX.md +1 -1
  2. package/dist/agents/codex-app-server.cjs +7 -1
  3. package/dist/agents/codex-app-server.cjs.map +1 -1
  4. package/dist/inspection/overview.cjs +7 -3
  5. package/dist/inspection/overview.cjs.map +1 -1
  6. package/dist/sandbox/incus/control.cjs +28 -1
  7. package/dist/sandbox/incus/control.cjs.map +1 -1
  8. package/dist/show/render.cjs +28 -15
  9. package/dist/show/render.cjs.map +1 -1
  10. package/dist/view/function-runtime.mjs +2 -2
  11. package/docs-site/zh/reference/cli.mdx +4 -2
  12. package/docs-site/zh/troubleshooting/debugging.mdx +20 -1
  13. package/docs-site/zh/tutorials/criteria-files.mdx +2 -0
  14. package/docs-site/zh/tutorials/evaluation-kinds.mdx +1 -1
  15. package/docs-site/zh/tutorials/rerun-and-cache.mdx +3 -1
  16. package/docs-site/zh/tutorials/viewing-results.mdx +43 -4
  17. package/node_modules/undici/docs/docs/api/Client.md +4 -0
  18. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +3 -2
  19. package/node_modules/undici/docs/docs/api/EventSource.md +4 -0
  20. package/node_modules/undici/lib/cache/memory-cache-store.js +1 -1
  21. package/node_modules/undici/lib/dispatcher/agent.js +7 -2
  22. package/node_modules/undici/lib/dispatcher/balanced-pool.js +1 -1
  23. package/node_modules/undici/lib/dispatcher/client-h1.js +12 -4
  24. package/node_modules/undici/lib/dispatcher/client-h2.js +43 -3
  25. package/node_modules/undici/lib/dispatcher/client.js +3 -2
  26. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +13 -1
  27. package/node_modules/undici/lib/dispatcher/dispatcher.js +10 -0
  28. package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +10 -3
  29. package/node_modules/undici/lib/dispatcher/proxy-agent.js +1 -1
  30. package/node_modules/undici/lib/dispatcher/round-robin-pool.js +1 -1
  31. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +3 -1
  32. package/node_modules/undici/lib/handler/cache-handler.js +8 -0
  33. package/node_modules/undici/lib/handler/decorator-handler.js +7 -1
  34. package/node_modules/undici/lib/handler/deduplication-handler.js +12 -6
  35. package/node_modules/undici/lib/handler/redirect-handler.js +8 -0
  36. package/node_modules/undici/lib/handler/retry-handler.js +83 -11
  37. package/node_modules/undici/lib/interceptor/dump.js +1 -1
  38. package/node_modules/undici/lib/mock/mock-agent.js +13 -1
  39. package/node_modules/undici/lib/mock/mock-utils.js +11 -10
  40. package/node_modules/undici/lib/web/cookies/parse.js +4 -2
  41. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +28 -1
  42. package/node_modules/undici/lib/web/eventsource/eventsource.js +4 -1
  43. package/node_modules/undici/lib/web/fetch/request.js +2 -3
  44. package/node_modules/undici/package.json +3 -3
  45. package/node_modules/undici/types/client.d.ts +10 -0
  46. package/package.json +4 -4
@@ -51,7 +51,7 @@ checkout cart/add-item ordinal 1 execute: input-identity-changed
51
51
  # 默认采用符合条件的 passed 与 failed Attempt
52
52
  npx niceeval exp checkout
53
53
 
54
- # 重跑 failed Attempt
54
+ # 单独写 --rerun 等价于 --rerun failed;已经 passed/scored 的 slot 仍会采用
55
55
  npx niceeval exp checkout --rerun
56
56
 
57
57
  # 重新执行选中范围内的全部 slot
@@ -60,6 +60,8 @@ npx niceeval exp checkout --rerun all
60
60
 
61
61
  改变 `--rerun` 只影响本次 Invocation,不会修改已有 Run 或 Attempt。
62
62
 
63
+ 不要在 `exp` 仍运行时并发执行 `show --experiment`:Experiment 可能尚未产生任何已发布结果。等待命令完成,从 receipt 复制本次 `runIds`,再用 `niceeval show --run <run-id>` 审阅确定的 Run。若目的是重做所有已通过或已评分的 slot,必须明确写 `--rerun all`;不带值的 `--rerun` 不会做到这一点。
64
+
63
65
  ## 明确接受已有 Attempt
64
66
 
65
67
  身份变化不一定表示历史结果已经失效,但 NiceEval 不会替操作者猜测。先从 dry run 或结果页取得完整 locator,核对候选 Attempt;确认它在新配置下仍然成立时,明确接受它:
@@ -12,7 +12,7 @@ description: "在终端用 show 逐层查看结果;在浏览器用 View 审阅
12
12
  niceeval show
13
13
  ```
14
14
 
15
- 不带 selector 时,`show` 输出当前 PublicationCutoff 的 Overview。它按 Experiment、评估用例和 Attempt 展开汇总,并保留通过率、分数、coverage、缺失项和 issues。先从这里找出需要继续查看的 Experiment、Run 或 Attempt locator。
15
+ 不带 selector 时,`show` 输出当前 PublicationCutoff 的 Overview。它按 Experiment、评估用例和 Attempt 展开汇总,并保留适用的通过率或分数、coverage、缺失项和 issues。纯计分制显示 `Outcomes`、`scored` 与 `Score`,不显示 `passed` 或通过率。先从这里找出需要继续查看的 Experiment、Run 或 Attempt locator。
16
16
 
17
17
  Overview 来自项目唯一的 `.niceeval/record.sqlite` 中已发布的结果。即使当前源码或当前安装候选的 identity 已经改变,
18
18
  `show` 也会保留每个逻辑 slot 的最新结果,不会把存在的历史显示为 `Observed 0/0`。这表示结果可查看,
@@ -51,6 +51,45 @@ niceeval show @<locator> --execution --expand <stable-id>
51
51
 
52
52
  `--timing` 显示活动的顺序和耗时,`--usage` 显示 token、请求和成本总计,`--diff` 显示已捕获的文件变更窗口。详情只读取 Record 已发布的事实;`partial`、`not-recorded` 或 `unavailable` 表示实际可用范围,不能当作零值或失败。
53
53
 
54
+ 默认 Overview 会隐藏没有问题的 Attempt,让错误和下一条下钻命令留在一屏内。看到 `Attempts hidden` 时,使用输出建议的 `show --experiment <experiment-id>` 缩小范围;需要显示该范围的全部 Attempt 时再加 `--all`。隐藏只影响终端排版,不表示 Record 缺少结果。
55
+
56
+ ## 从 Show 进入 Assertion 详情
57
+
58
+ `show @<locator>` 适合先找出 Assertion 名称和 Entry ID,但不展开每项 contribution、measurement 或 Judge rationale。需要解释具体得分或失败原因时,转到结构化 `query`,不要解析 `show` 的终端文本。
59
+
60
+ 先动态发现当前版本支持的 operation ID,再取得该 Attempt 的 Assertion 清单。`discover` 是紧凑目录,不包含 request schema;下面的请求就是 `attempt.get` 的最小形式,可先交给 `query explain` 校验:
61
+
62
+ ```sh
63
+ niceeval query discover
64
+
65
+ niceeval query run --request - <<'JSON'
66
+ {
67
+ "protocol": "niceeval.query/v1",
68
+ "operation": {
69
+ "kind": "attempt.get",
70
+ "locator": "@<locator>"
71
+ }
72
+ }
73
+ JSON
74
+ ```
75
+
76
+ 从返回的 `assertions.entries` 选择一个 `entryId`,再读取这一项:
77
+
78
+ ```sh
79
+ niceeval query run --request - <<'JSON'
80
+ {
81
+ "protocol": "niceeval.query/v1",
82
+ "operation": {
83
+ "kind": "attempt.assertion.detail",
84
+ "locator": "@<locator>",
85
+ "entryId": "<entry-id>"
86
+ }
87
+ }
88
+ JSON
89
+ ```
90
+
91
+ 详情中的 contribution 说明这一项获得多少分,evaluation 保存实际测量结果,explanation retention 则说明 Judge rationale 等解释材料是否可用。字段不存在或状态为 `unavailable` 时,按返回的 reason 解释,不能从其它字段猜值。
92
+
54
93
  ## 正确解释完成、判定与分数
55
94
 
56
95
  先确认 Attempt 是否形成业务结果,再解释它的质量:
@@ -58,9 +97,9 @@ niceeval show @<locator> --execution --expand <stable-id>
58
97
  - `errored`:执行链中断,没有形成可评分的业务结论。
59
98
  - `failed`:执行完成,但通过制评估的业务断言没有满足。
60
99
  - `passed`:通过制评估满足了判定条件。仍需检查 warning、coverage 和证据限制。
61
- - Score Eval:评分过程完成后读取 `Score` 和逐项 Assertion。Overview 中显示完成或通过,不等于取得满分。
100
+ - Score Eval:评分过程完成后读取 `Score` 和逐项 contribution。计分制没有 Attempt Verdict,不得用 `passed` 或通过率解释质量;执行中断时只报告 `errored` 和可用的 partial score。
62
101
 
63
- 比较结果时使用同一种评估、相同分母和相同证据覆盖。不要只比较 Overview 的通过率,也不要把 `unavailable` 当作零分。需要解释一项得失时,固定同一个 Attempt locator,交叉查看 overview、`--execution` 与 `--source`。
102
+ 比较结果时使用同一种评估、相同分母和相同证据覆盖。不要把 `unavailable` 当作零分。需要解释一项得失时,固定同一个 Attempt locator,交叉查看 overview、`--execution`、`--source` 与 `attempt.assertion.detail`。
64
103
 
65
104
  ## 在浏览器中审阅
66
105
 
@@ -83,7 +122,7 @@ niceeval query explain --request request.json
83
122
  niceeval query run --request request.json
84
123
  ```
85
124
 
86
- `query` 是给 AI、脚本和 CI 使用的机器入口,只输出 `niceeval.query/v1` 的结构化 JSON,不提供适合人阅读的终端排版。先从 `discover` 返回的 catalog 取得 operation schema,再构造 request。`run` 向 stdout 写一个 `niceeval.query/v1` document。不要解析 View 页面,也不要使用未公开的 Record 文件。
125
+ `query` 是给 AI、脚本和 CI 使用的机器入口,只输出 `niceeval.query/v1` 的结构化 JSON,不提供适合人阅读的终端排版。先从 `discover` 返回的紧凑 catalog 确认 operation ID,再按文档中的最小请求构造 request,并用 `explain` 校验。`run` 向 stdout 写一个 `niceeval.query/v1` document。不要解析 View 页面,也不要使用未公开的 Record 文件。
87
126
 
88
127
  常见读取任务由固定 operation 覆盖:列出 Run、取得 Run summary、读取 Attempt detail、trace、diff、sources、artifacts 或比较两个 Run。查看 [CLI](/zh/reference/cli) 获取完整说明。
89
128
 
@@ -147,6 +147,10 @@ added: v1.0.0
147
147
  WebSocket messages. Applied to uncompressed messages, compressed frame
148
148
  payloads, and decompressed (`permessage-deflate`) messages. Set to `0` to
149
149
  disable the limit. **Default:** `134217728`.
150
+ * `eventSource` {Object} (optional) EventSource-specific configuration.
151
+ * `maxEventSize` {number} The maximum allowed event size, in bytes, for
152
+ EventSource messages. Set to `0` to disable the limit.
153
+ **Default:** `buffer.kStringMaxLength`.
150
154
  * Returns: {Client}
151
155
 
152
156
  Instantiating a `Client` does not open a connection; the connection is
@@ -25,8 +25,9 @@ it is used only for HTTPS requests.
25
25
  proxied. Each entry may include a leading dot or `*.` wildcard (for example
26
26
  `.example.com`) to match subdomains, and an optional `:port` suffix to restrict
27
27
  the match to a specific port. A request bypasses the proxy when its host equals
28
- an entry or is a subdomain of one. Setting `no_proxy` to `*` bypasses the proxy
29
- for every request.
28
+ an entry or is a subdomain of one. A trailing dot is ignored on both sides, so
29
+ `example.com.` and `example.com` match each other. Setting `no_proxy` to `*`
30
+ bypasses the proxy for every request.
30
31
 
31
32
  The uppercase variants `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are also
32
33
  honored. When both the lowercase and uppercase forms of a variable are set, the
@@ -66,6 +66,9 @@ added: v6.5.0
66
66
  wait before re-establishing a dropped connection. The server may override
67
67
  this value with a `retry` field. **Default:** `3000`.
68
68
 
69
+ EventSource-specific limits can be configured on the dispatcher using the
70
+ `eventSource` option. See [`Client`][] for details.
71
+
69
72
  Creates a new `EventSource` and immediately begins connecting to `url`. The
70
73
  request is sent with the `Accept: text/event-stream` header, a cache mode of
71
74
  `no-store`, and an initiator type of `other`.
@@ -349,6 +352,7 @@ eventSource.onerror = () => {
349
352
  ```
350
353
 
351
354
  [WHATWG-conformant]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
355
+ [`Client`]: Client.md#new-clienturl-options
352
356
  [`Dispatcher`]: Dispatcher.md#class-dispatcher
353
357
  [`addEventListener()`]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
354
358
  [server-sent events]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
@@ -179,7 +179,7 @@ class MemoryCacheStore extends EventEmitter {
179
179
 
180
180
  // Perform eviction
181
181
  for (const [key, entries] of store.#entries) {
182
- for (const entry of entries.splice(0, entries.length / 2)) {
182
+ for (const entry of entries.splice(0, Math.ceil(entries.length / 2))) {
183
183
  store.#size -= entry.size
184
184
  store.#count -= 1
185
185
  }
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const { InvalidArgumentError, MaxOriginsReachedError } = require('../core/errors')
4
- const { kBusy, kClients, kConnected, kRunning, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
4
+ const { kBusy, kClients, kConnected, kRunning, kPending, kClose, kDestroy, kDispatch, kUrl } = require('../core/symbols')
5
5
  const DispatcherBase = require('./dispatcher-base')
6
6
  const Pool = require('./pool')
7
7
  const Client = require('./client')
@@ -97,7 +97,12 @@ class Agent extends DispatcherBase {
97
97
  return
98
98
  }
99
99
 
100
- if (dispatcher[kConnected] > 0 || dispatcher[kBusy]) {
100
+ // A GOAWAY detaches the HTTP/2 session before requeued requests are
101
+ // dispatched on a replacement connection. At that point the pool has
102
+ // no connected clients and is not busy, but it still has pending work.
103
+ // Closing it here lets the replacement Client finish those requests
104
+ // and then destroys that new connection with ClientDestroyedError.
105
+ if (dispatcher[kConnected] > 0 || dispatcher[kBusy] || dispatcher[kPending] > 0) {
101
106
  return
102
107
  }
103
108
 
@@ -54,7 +54,7 @@ class BalancedPool extends PoolBase {
54
54
  throw new InvalidArgumentError('factory must be a function.')
55
55
  }
56
56
 
57
- super()
57
+ super(opts)
58
58
 
59
59
  this[kOptions] = { ...util.deepClone(opts) }
60
60
  this[kIndex] = -1
@@ -1052,7 +1052,7 @@ function onSocketClose () {
1052
1052
 
1053
1053
  function clearIdleSocketValidation (socket) {
1054
1054
  if (socket[kIdleSocketValidationTimeout]) {
1055
- clearTimeout(socket[kIdleSocketValidationTimeout])
1055
+ clearImmediate(socket[kIdleSocketValidationTimeout])
1056
1056
  socket[kIdleSocketValidationTimeout] = null
1057
1057
  }
1058
1058
 
@@ -1061,15 +1061,23 @@ function clearIdleSocketValidation (socket) {
1061
1061
 
1062
1062
  function scheduleIdleSocketValidation (client, socket) {
1063
1063
  socket[kIdleSocketValidation] = 1
1064
- socket[kIdleSocketValidationTimeout] = setTimeout(() => {
1064
+ // Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
1065
+ // already pending on this idle keep-alive socket are processed before the
1066
+ // next request is written (GHSA-35p6-xmwp-9g52).
1067
+ //
1068
+ // setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
1069
+ // (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
1070
+ // block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
1071
+ // A ref'd Immediate both keeps the pending request alive and makes poll
1072
+ // return immediately — the hybrid those issues asked for.
1073
+ socket[kIdleSocketValidationTimeout] = setImmediate(() => {
1065
1074
  socket[kIdleSocketValidationTimeout] = null
1066
1075
  socket[kIdleSocketValidation] = 2
1067
1076
 
1068
1077
  if (client[kSocket] === socket && !socket.destroyed) {
1069
1078
  client[kResume]()
1070
1079
  }
1071
- }, 0)
1072
- socket[kIdleSocketValidationTimeout].unref?.()
1080
+ })
1073
1081
  }
1074
1082
 
1075
1083
  /**
@@ -10,7 +10,8 @@ const {
10
10
  InformationalError,
11
11
  InvalidArgumentError,
12
12
  HeadersTimeoutError,
13
- BodyTimeoutError
13
+ BodyTimeoutError,
14
+ ResponseExceededMaxSizeError
14
15
  } = require('../core/errors.js')
15
16
  const {
16
17
  kUrl,
@@ -39,7 +40,8 @@ const {
39
40
  kRemoteSettings,
40
41
  kHTTP2Stream,
41
42
  kHTTP2SessionState,
42
- kHTTP2Options
43
+ kHTTP2Options,
44
+ kMaxResponseSize
43
45
  } = require('../core/symbols.js')
44
46
  const { channels } = require('../core/diagnostics.js')
45
47
 
@@ -1022,9 +1024,11 @@ function writeH2 (client, request) {
1022
1024
  const state = {
1023
1025
  abort: null,
1024
1026
  body: request.body,
1027
+ bytesRead: 0,
1025
1028
  client,
1026
1029
  contentLength: null,
1027
1030
  expectsPayload: false,
1031
+ maxResponseSize: client[kMaxResponseSize],
1028
1032
  request,
1029
1033
  headersTimeout,
1030
1034
  bodyTimeout,
@@ -1260,6 +1264,7 @@ function writeH2 (client, request) {
1260
1264
  // become unreachable once the stream closes, so plain `on` avoids the
1261
1265
  // per-listener `once` wrapper allocation.
1262
1266
  stream.on('response', onResponse)
1267
+ stream.on('headers', onInterimResponse)
1263
1268
  stream.on('end', onEnd)
1264
1269
  stream.on('error', onError)
1265
1270
  stream.on('frameError', onFrameError)
@@ -1280,6 +1285,7 @@ function removeRequestStreamListeners (stream) {
1280
1285
  stream.off('error', noop)
1281
1286
  stream.off('continue', writeBodyH2)
1282
1287
  stream.off('response', onResponse)
1288
+ stream.off('headers', onInterimResponse)
1283
1289
  stream.off('end', onEnd)
1284
1290
  stream.off('error', onError)
1285
1291
  stream.off('frameError', onFrameError)
@@ -1322,17 +1328,51 @@ function onData (chunk) {
1322
1328
  return
1323
1329
  }
1324
1330
 
1325
- const { request } = state
1331
+ const { request, maxResponseSize } = state
1326
1332
 
1327
1333
  if (request.aborted || request.completed) {
1328
1334
  return
1329
1335
  }
1330
1336
 
1337
+ if (maxResponseSize > -1 && state.bytesRead + chunk.length > maxResponseSize) {
1338
+ // Unlike HTTP/1.1, which destroys the socket because it cannot abandon one
1339
+ // response without losing framing, resetting the offending stream leaves
1340
+ // the session usable for its siblings.
1341
+ state.abort(new ResponseExceededMaxSizeError())
1342
+ return
1343
+ }
1344
+
1345
+ state.bytesRead += chunk.length
1346
+
1331
1347
  if (request.onResponseData(chunk) === false) {
1332
1348
  stream.pause()
1333
1349
  }
1334
1350
  }
1335
1351
 
1352
+ function onInterimResponse (headers) {
1353
+ const stream = this
1354
+ const state = stream[kRequestStreamState]
1355
+
1356
+ if (state == null) {
1357
+ return
1358
+ }
1359
+
1360
+ const { request } = state
1361
+
1362
+ if (request.aborted || request.completed) {
1363
+ return
1364
+ }
1365
+
1366
+ // node http2 emits 'headers' for interim (1xx) informational responses,
1367
+ // while the final response arrives via 'response'. Forward these to the
1368
+ // handler so that onInfo is invoked, matching the HTTP/1 behaviour and the
1369
+ // documented onInfo contract.
1370
+ const statusCode = headers[HTTP2_HEADER_STATUS]
1371
+ delete headers[HTTP2_HEADER_STATUS]
1372
+
1373
+ request.onResponseStart(Number(statusCode), headers, noop, '')
1374
+ }
1375
+
1336
1376
  function onResponse (headers) {
1337
1377
  const stream = this
1338
1378
  const state = stream[kRequestStreamState]
@@ -137,7 +137,8 @@ class Client extends DispatcherBase {
137
137
  connectionWindowSize,
138
138
  pingInterval,
139
139
  webSocket,
140
- h2Options
140
+ h2Options,
141
+ eventSource
141
142
  } = {}) {
142
143
  if (keepAlive !== undefined) {
143
144
  throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
@@ -276,7 +277,7 @@ class Client extends DispatcherBase {
276
277
  }
277
278
  }
278
279
 
279
- super({ webSocket })
280
+ super({ webSocket, eventSource })
280
281
 
281
282
  if (typeof connect !== 'function') {
282
283
  connect = buildConnector({
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const buffer = require('node:buffer')
3
4
  const Dispatcher = require('./dispatcher')
4
5
  const {
5
6
  ClientDestroyedError,
@@ -11,6 +12,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy
11
12
  const kOnDestroyed = Symbol('onDestroyed')
12
13
  const kOnClosed = Symbol('onClosed')
13
14
  const kWebSocketOptions = Symbol('webSocketOptions')
15
+ const kEventSourceOptions = Symbol('eventSourceOptions')
14
16
 
15
17
  class DispatcherBase extends Dispatcher {
16
18
  /** @type {boolean} */
@@ -31,10 +33,11 @@ class DispatcherBase extends Dispatcher {
31
33
  constructor (opts) {
32
34
  super()
33
35
  this[kWebSocketOptions] = opts?.webSocket ?? {}
36
+ this[kEventSourceOptions] = opts?.eventSource ?? {}
34
37
  }
35
38
 
36
39
  /**
37
- * @returns {import('../../types/dispatcher').WebSocketOptions}
40
+ * @returns {import('../../types/client').Client.WebSocketOptions}
38
41
  */
39
42
  get webSocketOptions () {
40
43
  return {
@@ -43,6 +46,15 @@ class DispatcherBase extends Dispatcher {
43
46
  }
44
47
  }
45
48
 
49
+ /**
50
+ * @returns {import('../../types/client').Client.EventSourceOptions}
51
+ */
52
+ get eventSourceOptions () {
53
+ return {
54
+ maxEventSize: this[kEventSourceOptions].maxEventSize ?? buffer.kStringMaxLength
55
+ }
56
+ }
57
+
46
58
  /** @returns {boolean} */
47
59
  get destroyed () {
48
60
  return this[kDestroyed]
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
  const EventEmitter = require('node:events')
3
+ const { kUrl } = require('../core/symbols')
3
4
 
4
5
  class Dispatcher extends EventEmitter {
5
6
  dispatch () {
@@ -35,6 +36,15 @@ class Dispatcher extends EventEmitter {
35
36
  }
36
37
  }
37
38
 
39
+ const originalDispatch = dispatch
40
+ const self = this
41
+ dispatch = function (opts, handler) {
42
+ if (opts && typeof opts === 'object' && !opts.origin && self[kUrl]) {
43
+ opts = Object.assign({}, opts, { origin: self[kUrl].origin })
44
+ }
45
+ return originalDispatch(opts, handler)
46
+ }
47
+
38
48
  return new Proxy(this, {
39
49
  get: (target, key) => key === 'dispatch' ? dispatch : target[key]
40
50
  })
@@ -16,7 +16,7 @@ class EnvHttpProxyAgent extends DispatcherBase {
16
16
  #opts = null
17
17
 
18
18
  constructor (opts = {}) {
19
- super()
19
+ super(opts)
20
20
  this.#opts = opts
21
21
 
22
22
  const { httpProxy, httpsProxy, noProxy, ...agentOpts } = opts
@@ -69,6 +69,13 @@ class EnvHttpProxyAgent extends DispatcherBase {
69
69
  // brackets from IPv6 literals (e.g. "[::1]" -> "::1") so that the
70
70
  // result matches the unbracketed form stored by #parseNoProxy.
71
71
  hostname = hostname.replace(/:\d*$/, '').replace(/^\[(.+)\]$/, '$1').toLowerCase()
72
+ // Drop a trailing dot: it only marks the fully qualified form of a domain
73
+ // name ("example.com." and "example.com" are the same name, RFC 1034 root
74
+ // label). This runs on every dispatch, so it is a charCode check rather
75
+ // than a third regex. `length > 1` leaves the degenerate host "." alone.
76
+ if (hostname.length > 1 && hostname.charCodeAt(hostname.length - 1) === 46) {
77
+ hostname = hostname.slice(0, -1)
78
+ }
72
79
  port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0
73
80
  if (!this.#shouldProxy(hostname, port)) {
74
81
  return this[kNoProxyAgent]
@@ -143,8 +150,8 @@ class EnvHttpProxyAgent extends DispatcherBase {
143
150
  }
144
151
 
145
152
  noProxyEntries.push({
146
- // strip leading dot or asterisk with dot
147
- hostname: hostname.replace(/^\*?\./, '').toLowerCase(),
153
+ // strip leading dot or asterisk with dot, and any trailing dot
154
+ hostname: hostname.replace(/^\*?\./, '').replace(/^(.+)\.$/, '$1').toLowerCase(),
148
155
  port
149
156
  })
150
157
  }
@@ -120,7 +120,7 @@ class ProxyAgent extends DispatcherBase {
120
120
 
121
121
  const { proxyTunnel, connectTimeout } = opts
122
122
 
123
- super()
123
+ super(opts)
124
124
 
125
125
  const url = this.#getUrl(opts)
126
126
  const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url
@@ -65,7 +65,7 @@ class RoundRobinPool extends PoolBase {
65
65
  })
66
66
  }
67
67
 
68
- super()
68
+ super(options)
69
69
 
70
70
  this[kConnections] = connections || null
71
71
  this[kUrl] = util.parseOrigin(origin)
@@ -29,7 +29,7 @@ let experimentalWarningEmitted = false
29
29
  */
30
30
  class Socks5ProxyAgent extends DispatcherBase {
31
31
  constructor (proxyUrl, options = {}) {
32
- super()
32
+ super(options)
33
33
 
34
34
  // Emit experimental warning only once
35
35
  if (!experimentalWarningEmitted) {
@@ -115,6 +115,7 @@ class Socks5ProxyAgent extends DispatcherBase {
115
115
  const authenticationReady = Promise.withResolvers()
116
116
 
117
117
  const authenticationTimeout = setTimeout(() => {
118
+ socks5Client.destroy()
118
119
  authenticationReady.reject(new Error('SOCKS5 authentication timeout'))
119
120
  }, 5000)
120
121
 
@@ -148,6 +149,7 @@ class Socks5ProxyAgent extends DispatcherBase {
148
149
  const connectionReady = Promise.withResolvers()
149
150
 
150
151
  const connectionTimeout = setTimeout(() => {
152
+ socks5Client.destroy()
151
153
  connectionReady.reject(new Error('SOCKS5 connection timeout'))
152
154
  }, 5000)
153
155
 
@@ -173,6 +173,14 @@ class CacheHandler {
173
173
  this.#handler.onRequestStart?.(controller, context)
174
174
  }
175
175
 
176
+ onBodySent (chunk) {
177
+ this.#handler.onBodySent?.(chunk)
178
+ }
179
+
180
+ onRequestSent () {
181
+ this.#handler.onRequestSent?.()
182
+ }
183
+
176
184
  onRequestUpgrade (controller, statusCode, headers, socket) {
177
185
  this.#handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
178
186
  }
@@ -62,5 +62,11 @@ module.exports = class DecoratorHandler {
62
62
  /**
63
63
  * @deprecated
64
64
  */
65
- onBodySent () {}
65
+ onBodySent (...args) {
66
+ return this.#handler.onBodySent?.(...args)
67
+ }
68
+
69
+ onRequestSent (...args) {
70
+ return this.#handler.onRequestSent?.(...args)
71
+ }
66
72
  }
@@ -365,12 +365,22 @@ class DeduplicationHandler {
365
365
  get aborted () { return state.aborted },
366
366
  get reason () { return state.reason },
367
367
  abort: (reason) => {
368
+ if (state.aborted) {
369
+ return
370
+ }
371
+
368
372
  state.aborted = true
369
373
  state.reason = reason ?? null
370
374
  waitingHandler.done = true
371
375
  waitingHandler.pendingTrailers = null
372
376
  waitingHandler.bufferedChunks = []
373
377
  waitingHandler.bufferedBytes = 0
378
+
379
+ try {
380
+ handler.onResponseError?.(waitingHandler.controller, state.reason ?? new RequestAbortedError())
381
+ } catch {
382
+ // Ignore errors from waiting handlers
383
+ }
374
384
  }
375
385
  }
376
386
 
@@ -444,12 +454,8 @@ class DeduplicationHandler {
444
454
  waitingHandler.bufferedChunks = []
445
455
  waitingHandler.bufferedBytes = 0
446
456
 
447
- try {
448
- waitingHandler.controller.abort(err)
449
- waitingHandler.handler.onResponseError?.(waitingHandler.controller, err)
450
- } catch {
451
- // Ignore errors from waiting handlers
452
- }
457
+ // controller.abort(err) notifies the handler via onResponseError
458
+ waitingHandler.controller.abort(err)
453
459
  }
454
460
 
455
461
  #pruneDoneWaitingHandlers () {
@@ -43,6 +43,14 @@ class RedirectHandler {
43
43
  this.handler.onRequestStart?.(controller, { ...context, history: this.history })
44
44
  }
45
45
 
46
+ onBodySent (chunk) {
47
+ this.handler.onBodySent?.(chunk)
48
+ }
49
+
50
+ onRequestSent () {
51
+ this.handler.onRequestSent?.()
52
+ }
53
+
46
54
  onRequestUpgrade (controller, statusCode, headers, socket) {
47
55
  this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
48
56
  }