@entelligentsia/forgecli 0.7.10 → 0.8.4

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 (227) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/CHANGELOG-forge-plugin.md +70 -0
  3. package/dist/CHANGELOG-pi.md +63 -0
  4. package/dist/bin/argv.d.ts +2 -2
  5. package/dist/bin/argv.js +10 -0
  6. package/dist/bin/argv.js.map +1 -1
  7. package/dist/bin/env-defaults.d.ts +1 -0
  8. package/dist/bin/env-defaults.js +13 -0
  9. package/dist/bin/env-defaults.js.map +1 -0
  10. package/dist/bin/forge.js +9 -0
  11. package/dist/bin/forge.js.map +1 -1
  12. package/dist/bin/update-cli.d.ts +9 -0
  13. package/dist/bin/update-cli.js +120 -0
  14. package/dist/bin/update-cli.js.map +1 -0
  15. package/dist/extensions/forgecli/index.js +3 -3
  16. package/dist/extensions/forgecli/index.js.map +1 -1
  17. package/dist/extensions/forgecli/update-check.js +1 -1
  18. package/dist/extensions/forgecli/update-check.js.map +1 -1
  19. package/dist/extensions/forgecli/whats-new-widget.d.ts +5 -5
  20. package/dist/extensions/forgecli/whats-new-widget.js +11 -11
  21. package/dist/extensions/forgecli/whats-new-widget.js.map +1 -1
  22. package/dist/extensions/forgecli/whats-new.js +6 -5
  23. package/dist/extensions/forgecli/whats-new.js.map +1 -1
  24. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  25. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +27 -98
  26. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  27. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +62 -132
  28. package/node_modules/@earendil-works/pi-ai/dist/models.generated.js.map +1 -1
  29. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.d.ts.map +1 -1
  30. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +25 -15
  31. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  32. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  33. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js +1 -0
  34. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.js.map +1 -1
  35. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
  36. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js +17 -1
  37. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
  38. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  39. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js +8 -2
  40. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-completions.js.map +1 -1
  41. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
  42. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js +17 -1
  43. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-responses.js.map +1 -1
  44. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
  45. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js +8 -1
  46. package/node_modules/@earendil-works/pi-ai/dist/providers/simple-options.js.map +1 -1
  47. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  48. package/node_modules/@earendil-works/pi-coding-agent/CHANGELOG.md +63 -0
  49. package/node_modules/@earendil-works/pi-coding-agent/README.md +1 -1
  50. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/config-selector.d.ts.map +1 -1
  51. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/config-selector.js +1 -1
  52. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/config-selector.js.map +1 -1
  53. package/node_modules/@earendil-works/pi-coding-agent/dist/cli.d.ts.map +1 -1
  54. package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js +6 -10
  55. package/node_modules/@earendil-works/pi-coding-agent/dist/cli.js.map +1 -1
  56. package/node_modules/@earendil-works/pi-coding-agent/dist/config.d.ts.map +1 -1
  57. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js +12 -3
  58. package/node_modules/@earendil-works/pi-coding-agent/dist/config.js.map +1 -1
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts +1 -0
  60. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  61. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +30 -15
  62. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  63. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.d.ts +3 -3
  64. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  65. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +23 -13
  66. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  67. package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.d.ts +4 -0
  68. package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  69. package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js +58 -38
  70. package/node_modules/@earendil-works/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  71. package/node_modules/@earendil-works/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  72. package/node_modules/@earendil-works/pi-coding-agent/dist/core/slash-commands.js +0 -1
  73. package/node_modules/@earendil-works/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  74. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  75. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js +3 -2
  76. package/node_modules/@earendil-works/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  77. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.d.ts +2 -2
  78. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
  79. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js +7 -4
  80. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/config-selector.js.map +1 -1
  81. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  82. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +6 -2
  83. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  84. package/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.d.ts.map +1 -1
  85. package/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js +3 -4
  86. package/node_modules/@earendil-works/pi-coding-agent/dist/package-manager-cli.js.map +1 -1
  87. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/changelog.d.ts.map +1 -1
  88. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/changelog.js +2 -2
  89. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/changelog.js.map +1 -1
  90. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/child-process.d.ts +7 -1
  91. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/child-process.d.ts.map +1 -1
  92. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/child-process.js +60 -7
  93. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/child-process.js.map +1 -1
  94. package/node_modules/@earendil-works/pi-coding-agent/docs/packages.md +2 -2
  95. package/node_modules/@earendil-works/pi-coding-agent/docs/settings.md +1 -3
  96. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-anthropic/package.json +1 -1
  97. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  98. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/sandbox/package.json +1 -1
  99. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/with-deps/package.json +1 -1
  100. package/node_modules/@earendil-works/pi-coding-agent/package.json +6 -6
  101. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  102. package/node_modules/@protobufjs/fetch/CHANGELOG.md +8 -0
  103. package/node_modules/@protobufjs/fetch/index.d.ts +7 -7
  104. package/node_modules/@protobufjs/fetch/index.js +4 -7
  105. package/node_modules/@protobufjs/fetch/package.json +7 -5
  106. package/node_modules/@protobufjs/fetch/tests/data/file.txt +1 -0
  107. package/node_modules/@protobufjs/fetch/tests/index.js +150 -8
  108. package/node_modules/@protobufjs/fetch/util/fs.js +11 -0
  109. package/node_modules/@protobufjs/inquire/CHANGELOG.md +8 -0
  110. package/node_modules/@protobufjs/inquire/index.d.ts +1 -0
  111. package/node_modules/@protobufjs/inquire/index.js +1 -0
  112. package/node_modules/@protobufjs/inquire/package.json +1 -1
  113. package/node_modules/protobufjs/dist/light/protobuf.js +187 -153
  114. package/node_modules/protobufjs/dist/light/protobuf.js.map +1 -1
  115. package/node_modules/protobufjs/dist/light/protobuf.min.js +3 -3
  116. package/node_modules/protobufjs/dist/light/protobuf.min.js.map +1 -1
  117. package/node_modules/protobufjs/dist/minimal/protobuf.js +14 -5
  118. package/node_modules/protobufjs/dist/minimal/protobuf.js.map +1 -1
  119. package/node_modules/protobufjs/dist/minimal/protobuf.min.js +3 -3
  120. package/node_modules/protobufjs/dist/minimal/protobuf.min.js.map +1 -1
  121. package/node_modules/protobufjs/dist/protobuf.js +207 -173
  122. package/node_modules/protobufjs/dist/protobuf.js.map +1 -1
  123. package/node_modules/protobufjs/dist/protobuf.min.js +3 -3
  124. package/node_modules/protobufjs/dist/protobuf.min.js.map +1 -1
  125. package/node_modules/protobufjs/package.json +6 -3
  126. package/node_modules/protobufjs/src/util/fs.js +11 -0
  127. package/node_modules/protobufjs/src/util/minimal.js +10 -2
  128. package/node_modules/protobufjs/src/util.js +1 -1
  129. package/node_modules/undici/README.md +14 -5
  130. package/node_modules/undici/docs/docs/api/Client.md +4 -2
  131. package/node_modules/undici/docs/docs/api/Dispatcher.md +62 -27
  132. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +7 -5
  133. package/node_modules/undici/docs/docs/api/H2CClient.md +1 -1
  134. package/node_modules/undici/docs/docs/api/RedirectHandler.md +14 -9
  135. package/node_modules/undici/docs/docs/api/RetryAgent.md +0 -1
  136. package/node_modules/undici/docs/docs/api/RetryHandler.md +12 -14
  137. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +23 -0
  138. package/node_modules/undici/docs/docs/best-practices/migrating-from-v7-to-v8.md +231 -0
  139. package/node_modules/undici/index.js +4 -2
  140. package/node_modules/undici/lib/api/api-connect.js +13 -11
  141. package/node_modules/undici/lib/api/api-pipeline.js +26 -13
  142. package/node_modules/undici/lib/api/api-request.js +45 -21
  143. package/node_modules/undici/lib/api/api-stream.js +81 -20
  144. package/node_modules/undici/lib/api/api-upgrade.js +21 -11
  145. package/node_modules/undici/lib/api/readable.js +3 -2
  146. package/node_modules/undici/lib/cache/memory-cache-store.js +1 -1
  147. package/node_modules/undici/lib/cache/sqlite-cache-store.js +6 -4
  148. package/node_modules/undici/lib/core/connect.js +17 -1
  149. package/node_modules/undici/lib/core/constants.js +1 -24
  150. package/node_modules/undici/lib/core/errors.js +2 -2
  151. package/node_modules/undici/lib/core/request.js +115 -18
  152. package/node_modules/undici/lib/core/socks5-client.js +24 -9
  153. package/node_modules/undici/lib/core/socks5-utils.js +32 -23
  154. package/node_modules/undici/lib/core/symbols.js +1 -0
  155. package/node_modules/undici/lib/core/util.js +70 -43
  156. package/node_modules/undici/lib/dispatcher/agent.js +47 -33
  157. package/node_modules/undici/lib/dispatcher/balanced-pool.js +21 -26
  158. package/node_modules/undici/lib/dispatcher/client-h1.js +98 -39
  159. package/node_modules/undici/lib/dispatcher/client-h2.js +603 -272
  160. package/node_modules/undici/lib/dispatcher/client.js +12 -5
  161. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +24 -5
  162. package/node_modules/undici/lib/dispatcher/dispatcher.js +0 -4
  163. package/node_modules/undici/lib/dispatcher/dispatcher1-wrapper.js +107 -0
  164. package/node_modules/undici/lib/dispatcher/h2c-client.js +5 -5
  165. package/node_modules/undici/lib/dispatcher/pool-base.js +28 -10
  166. package/node_modules/undici/lib/dispatcher/pool.js +31 -6
  167. package/node_modules/undici/lib/dispatcher/proxy-agent.js +38 -13
  168. package/node_modules/undici/lib/dispatcher/round-robin-pool.js +31 -9
  169. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +95 -80
  170. package/node_modules/undici/lib/global.js +13 -1
  171. package/node_modules/undici/lib/handler/cache-handler.js +16 -8
  172. package/node_modules/undici/lib/handler/decorator-handler.js +1 -2
  173. package/node_modules/undici/lib/handler/redirect-handler.js +5 -51
  174. package/node_modules/undici/lib/handler/retry-handler.js +15 -2
  175. package/node_modules/undici/lib/interceptor/cache.js +30 -17
  176. package/node_modules/undici/lib/interceptor/decompress.js +28 -2
  177. package/node_modules/undici/lib/interceptor/dns.js +1 -1
  178. package/node_modules/undici/lib/interceptor/redirect.js +3 -3
  179. package/node_modules/undici/lib/llhttp/llhttp-wasm.js +1 -1
  180. package/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +1 -1
  181. package/node_modules/undici/lib/mock/mock-agent.js +8 -8
  182. package/node_modules/undici/lib/mock/mock-call-history.js +15 -15
  183. package/node_modules/undici/lib/mock/mock-utils.js +37 -22
  184. package/node_modules/undici/lib/mock/snapshot-agent.js +16 -6
  185. package/node_modules/undici/lib/mock/snapshot-recorder.js +38 -3
  186. package/node_modules/undici/lib/util/cache.js +8 -7
  187. package/node_modules/undici/lib/util/runtime-features.js +3 -34
  188. package/node_modules/undici/lib/web/cache/cache.js +6 -8
  189. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +245 -150
  190. package/node_modules/undici/lib/web/fetch/body.js +3 -9
  191. package/node_modules/undici/lib/web/fetch/formdata-parser.js +17 -6
  192. package/node_modules/undici/lib/web/fetch/formdata.js +21 -2
  193. package/node_modules/undici/lib/web/fetch/index.js +214 -221
  194. package/node_modules/undici/lib/web/webidl/index.js +7 -9
  195. package/node_modules/undici/lib/web/websocket/frame.js +1 -7
  196. package/node_modules/undici/lib/web/websocket/permessage-deflate.js +13 -31
  197. package/node_modules/undici/lib/web/websocket/receiver.js +62 -22
  198. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +11 -17
  199. package/node_modules/undici/lib/web/websocket/websocket.js +6 -1
  200. package/node_modules/undici/package.json +9 -9
  201. package/node_modules/undici/types/agent.d.ts +0 -2
  202. package/node_modules/undici/types/client.d.ts +25 -19
  203. package/node_modules/undici/types/dispatcher.d.ts +7 -27
  204. package/node_modules/undici/types/dispatcher1-wrapper.d.ts +7 -0
  205. package/node_modules/undici/types/formdata.d.ts +0 -6
  206. package/node_modules/undici/types/h2c-client.d.ts +6 -6
  207. package/node_modules/undici/types/header.d.ts +5 -0
  208. package/node_modules/undici/types/index.d.ts +3 -1
  209. package/node_modules/undici/types/interceptors.d.ts +1 -1
  210. package/node_modules/undici/types/pool.d.ts +0 -2
  211. package/node_modules/undici/types/proxy-agent.d.ts +2 -2
  212. package/node_modules/undici/types/round-robin-pool.d.ts +0 -2
  213. package/node_modules/undici/types/snapshot-agent.d.ts +4 -0
  214. package/node_modules/undici/types/socks5-proxy-agent.d.ts +2 -2
  215. package/node_modules/undici/types/webidl.d.ts +0 -1
  216. package/package.json +7 -8
  217. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/custom-provider-anthropic/package-lock.json +0 -24
  218. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/sandbox/package-lock.json +0 -92
  219. package/node_modules/@earendil-works/pi-coding-agent/examples/extensions/with-deps/package-lock.json +0 -31
  220. package/node_modules/undici/lib/handler/unwrap-handler.js +0 -100
  221. package/node_modules/undici/lib/handler/wrap-handler.js +0 -105
  222. package/node_modules/undici/lib/llhttp/.gitkeep +0 -0
  223. package/node_modules/undici/lib/util/promise.js +0 -28
  224. package/skills/refresh-kb-links/SKILL.md +0 -217
  225. package/skills/store-custodian/SKILL.md +0 -163
  226. package/skills/store-query-grammar/SKILL.md +0 -145
  227. package/skills/store-query-nlp/SKILL.md +0 -110
@@ -28,6 +28,7 @@ const {
28
28
  kHTTP2Session,
29
29
  kHTTP2InitialWindowSize,
30
30
  kHTTP2ConnectionWindowSize,
31
+ kHostAuthority,
31
32
  kResume,
32
33
  kSize,
33
34
  kHTTPContext,
@@ -41,6 +42,11 @@ const {
41
42
  const { channels } = require('../core/diagnostics.js')
42
43
 
43
44
  const kOpenStreams = Symbol('open streams')
45
+ const kRequestStreamId = Symbol('request stream id')
46
+ const kRequestStream = Symbol('request stream')
47
+ const kRequestStreamCleanup = Symbol('request stream cleanup')
48
+ const kRequestStreamState = Symbol('request stream state')
49
+ const kReceivedGoAway = Symbol('received goaway')
44
50
 
45
51
  let extractBody
46
52
 
@@ -63,29 +69,75 @@ const {
63
69
  HTTP2_HEADER_EXPECT,
64
70
  HTTP2_HEADER_STATUS,
65
71
  HTTP2_HEADER_PROTOCOL,
66
- NGHTTP2_REFUSED_STREAM,
67
- NGHTTP2_CANCEL
72
+ NGHTTP2_NO_ERROR,
73
+ NGHTTP2_REFUSED_STREAM
68
74
  }
69
75
  } = http2
70
76
 
71
- function parseH2Headers (headers) {
72
- const result = []
73
-
74
- for (const [name, value] of Object.entries(headers)) {
75
- // h2 may concat the header value by array
76
- // e.g. Set-Cookie
77
- if (Array.isArray(value)) {
78
- for (const subvalue of value) {
79
- // we need to provide each header value of header name
80
- // because the headers handler expect name-value pair
81
- result.push(Buffer.from(name), Buffer.from(subvalue))
82
- }
83
- } else {
84
- result.push(Buffer.from(name), Buffer.from(value))
77
+ function getGoAwayError (session, errorCode) {
78
+ return session[kError] ||
79
+ (errorCode === NGHTTP2_NO_ERROR
80
+ ? new InformationalError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`)
81
+ : new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(session[kSocket])))
82
+ }
83
+
84
+ function getGoAwayPendingIdx (client, lastStreamID) {
85
+ const maxAcceptedStreamID = Number.isInteger(lastStreamID) ? lastStreamID : Number.MAX_SAFE_INTEGER
86
+
87
+ for (let i = client[kRunningIdx]; i < client[kPendingIdx]; i++) {
88
+ const request = client[kQueue][i]
89
+
90
+ if (request == null) {
91
+ continue
92
+ }
93
+
94
+ if (typeof request[kRequestStreamId] !== 'number' || request[kRequestStreamId] > maxAcceptedStreamID) {
95
+ return i
85
96
  }
86
97
  }
87
98
 
88
- return result
99
+ return client[kPendingIdx]
100
+ }
101
+
102
+ function detachRequestFromStream (request) {
103
+ request[kRequestStreamId] = null
104
+ request[kRequestStream] = null
105
+ request[kRequestStreamCleanup] = null
106
+ }
107
+
108
+ function bindRequestToStream (request, stream, cleanup) {
109
+ const previousCleanup = request[kRequestStreamCleanup]
110
+ const previousStream = request[kRequestStream]
111
+ detachRequestFromStream(request)
112
+ previousCleanup?.(previousStream)
113
+ request[kRequestStreamId] = stream.id
114
+ request[kRequestStream] = stream
115
+ request[kRequestStreamCleanup] = cleanup
116
+ }
117
+
118
+ function clearRequestStream (request) {
119
+ const cleanup = request[kRequestStreamCleanup]
120
+ const stream = request[kRequestStream]
121
+ detachRequestFromStream(request)
122
+ cleanup?.(stream)
123
+ }
124
+
125
+ function canRetryRequestAfterGoAway (request) {
126
+ const { body } = request
127
+
128
+ return body == null || util.isBuffer(body) || util.isBlobLike(body)
129
+ }
130
+
131
+ function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
132
+ const stream = request[kRequestStream]
133
+
134
+ clearRequestStream(request)
135
+
136
+ if (stream != null && !stream.destroyed && !stream.closed) {
137
+ try {
138
+ stream.close(code)
139
+ } catch {}
140
+ }
89
141
  }
90
142
 
91
143
  function connectH2 (client, socket) {
@@ -113,6 +165,7 @@ function connectH2 (client, socket) {
113
165
  interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
114
166
  }
115
167
  }
168
+ session[kReceivedGoAway] = false
116
169
  // We set it to true by default in a best-effort; however once connected to an H2 server
117
170
  // we will check if extended CONNECT protocol is supported or not
118
171
  // and set this value accordingly.
@@ -184,6 +237,14 @@ function connectH2 (client, socket) {
184
237
  * @returns {boolean}
185
238
  */
186
239
  busy (request) {
240
+ if (session[kRemoteSettings] === false && client[kRunning] > 0) {
241
+ return true
242
+ }
243
+
244
+ if (client[kRunning] >= client[kMaxConcurrentStreams]) {
245
+ return true
246
+ }
247
+
187
248
  if (request != null) {
188
249
  if (client[kRunning] > 0) {
189
250
  // We are already processing requests
@@ -273,7 +334,7 @@ function onHttp2SendPing (session) {
273
334
 
274
335
  function onPing (err, duration) {
275
336
  const client = this[kClient]
276
- const socket = this[kClient]
337
+ const socket = this[kSocket]
277
338
 
278
339
  if (err != null) {
279
340
  const error = new InformationalError(`HTTP/2: "PING" errored - type ${err.message}`)
@@ -313,48 +374,68 @@ function onHttp2SessionEnd () {
313
374
  *
314
375
  * @this {import('http2').ClientHttp2Session}
315
376
  * @param {number} errorCode
377
+ * @param {number} lastStreamID
316
378
  */
317
- function onHttp2SessionGoAway (errorCode) {
318
- // TODO(mcollina): Verify if GOAWAY implements the spec correctly:
319
- // https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
320
- // Specifically, we do not verify the "valid" stream id.
379
+ function onHttp2SessionGoAway (errorCode, lastStreamID) {
380
+ if (this[kReceivedGoAway]) {
381
+ return
382
+ }
383
+
384
+ this[kReceivedGoAway] = true
321
385
 
322
- const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${errorCode}`, util.getSocketInfo(this[kSocket]))
386
+ const err = getGoAwayError(this, errorCode)
323
387
  const client = this[kClient]
388
+ const previousPendingIdx = client[kPendingIdx]
389
+ const pendingIdx = getGoAwayPendingIdx(client, lastStreamID)
390
+ const retriableRequests = []
324
391
 
325
- client[kSocket] = null
326
- client[kHTTPContext] = null
392
+ for (let i = pendingIdx; i < previousPendingIdx; i++) {
393
+ const request = client[kQueue][i]
327
394
 
328
- // this is an HTTP2 session
329
- this.close()
330
- this[kHTTP2Session] = null
395
+ if (request != null) {
396
+ closeRequestStream(request)
331
397
 
332
- util.destroy(this[kSocket], err)
398
+ if (canRetryRequestAfterGoAway(request)) {
399
+ retriableRequests.push(request)
400
+ } else {
401
+ util.errorRequest(client, request, err)
402
+ }
403
+ }
404
+ }
333
405
 
334
- // Fail head of pipeline.
335
- if (client[kRunningIdx] < client[kQueue].length) {
336
- const request = client[kQueue][client[kRunningIdx]]
337
- client[kQueue][client[kRunningIdx]++] = null
338
- util.errorRequest(client, request, err)
339
- client[kPendingIdx] = client[kRunningIdx]
406
+ if (pendingIdx !== previousPendingIdx) {
407
+ const remainingPendingRequests = client[kQueue].slice(previousPendingIdx)
408
+ client[kQueue].length = pendingIdx
409
+ client[kQueue].push(...retriableRequests, ...remainingPendingRequests)
340
410
  }
341
411
 
342
- assert(client[kRunning] === 0)
412
+ if (client[kHTTP2Session] === this) {
413
+ client[kSocket] = null
414
+ client[kHTTPContext] = null
415
+ client[kHTTP2Session] = null
416
+ }
417
+
418
+ if (!this.closed && !this.destroyed) {
419
+ this.close()
420
+ }
421
+
422
+ client[kPendingIdx] = pendingIdx
343
423
 
344
424
  client.emit('disconnect', client[kUrl], [client], err)
345
- client.emit('connectionError', client[kUrl], [client], err)
346
425
 
347
426
  client[kResume]()
348
427
  }
349
428
 
350
429
  function onHttp2SessionClose () {
351
- const { [kClient]: client, [kHTTP2SessionState]: state } = this
352
- const { [kSocket]: socket } = client
430
+ const { [kClient]: client, [kHTTP2SessionState]: state, [kSocket]: socket } = this
353
431
 
354
- const err = this[kSocket][kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
432
+ const err = socket[kError] || this[kError] || new SocketError('closed', util.getSocketInfo(socket))
355
433
 
356
- client[kSocket] = null
357
- client[kHTTPContext] = null
434
+ if (client[kHTTP2Session] === this) {
435
+ client[kSocket] = null
436
+ client[kHTTPContext] = null
437
+ client[kHTTP2Session] = null
438
+ }
358
439
 
359
440
  if (state.ping.interval != null) {
360
441
  clearInterval(state.ping.interval)
@@ -376,15 +457,27 @@ function onHttp2SessionClose () {
376
457
  function onHttp2SocketClose () {
377
458
  const err = this[kError] || new SocketError('closed', util.getSocketInfo(this))
378
459
 
379
- const client = this[kHTTP2Session][kClient]
460
+ const session = this[kHTTP2Session]
461
+ const client = session[kClient]
462
+
463
+ if (client[kSocket] !== this) {
464
+ // Ignore stale socket closes from a detached GOAWAY session and from any
465
+ // session that has already been replaced. If the session was detached
466
+ // without a GOAWAY and there is no replacement yet, we still need the
467
+ // close event to flush the client state.
468
+ if (session[kReceivedGoAway] || (client[kHTTP2Session] != null && client[kHTTP2Session] !== session)) {
469
+ return
470
+ }
471
+ }
380
472
 
381
473
  client[kSocket] = null
382
474
  client[kHTTPContext] = null
383
-
384
- if (this[kHTTP2Session] !== null) {
385
- this[kHTTP2Session].destroy(err)
475
+ if (client[kHTTP2Session] === session) {
476
+ client[kHTTP2Session] = null
386
477
  }
387
478
 
479
+ session.destroy(err)
480
+
388
481
  client[kPendingIdx] = client[kRunningIdx]
389
482
 
390
483
  assert(client[kRunning] === 0)
@@ -410,30 +503,51 @@ function onSocketClose () {
410
503
  this[kClosed] = true
411
504
  }
412
505
 
506
+ function noop () {}
507
+
508
+ function closeStreamSession (stream) {
509
+ const session = stream[kHTTP2Session]
510
+
511
+ stream[kHTTP2Session] = null
512
+ session[kOpenStreams] -= 1
513
+ if (session[kOpenStreams] === 0) {
514
+ session.unref()
515
+ }
516
+ }
517
+
518
+ function onUpgradeStreamClose () {
519
+ this.off('error', noop)
520
+
521
+ const state = this[kRequestStreamState]
522
+ this[kRequestStreamState] = null
523
+
524
+ failUpgradeStream(state, new InformationalError('HTTP/2: stream closed before response headers'))
525
+ closeStreamSession(this)
526
+ }
527
+
528
+ function onRequestStreamClose () {
529
+ this.off('data', onData)
530
+ this.off('error', noop)
531
+ closeStreamSession(this)
532
+ this[kRequestStreamState] = null
533
+ }
534
+
413
535
  // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2
414
536
  function shouldSendContentLength (method) {
415
537
  return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT'
416
538
  }
417
539
 
418
- function writeH2 (client, request) {
419
- const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
420
- const session = client[kHTTP2Session]
421
- const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
422
- let { body } = request
423
-
424
- if (upgrade != null && upgrade !== 'websocket') {
425
- util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
426
- return false
427
- }
428
-
540
+ function buildRequestHeaders (reqHeaders) {
429
541
  const headers = {}
542
+
430
543
  for (let n = 0; n < reqHeaders.length; n += 2) {
431
544
  const key = reqHeaders[n + 0]
432
545
  const val = reqHeaders[n + 1]
546
+ const current = headers[key]
433
547
 
434
548
  if (key === 'cookie') {
435
- if (headers[key] != null) {
436
- headers[key] = Array.isArray(headers[key]) ? (headers[key].push(val), headers[key]) : [headers[key], val]
549
+ if (current != null) {
550
+ headers[key] = Array.isArray(current) ? (current.push(val), current) : [current, val]
437
551
  } else {
438
552
  headers[key] = val
439
553
  }
@@ -441,30 +555,159 @@ function writeH2 (client, request) {
441
555
  continue
442
556
  }
443
557
 
444
- if (Array.isArray(val)) {
445
- for (let i = 0; i < val.length; i++) {
446
- if (headers[key]) {
447
- headers[key] += `, ${val[i]}`
448
- } else {
449
- headers[key] = val[i]
450
- }
451
- }
452
- } else if (headers[key]) {
453
- headers[key] += `, ${val}`
454
- } else {
455
- headers[key] = val
558
+ if (typeof val === 'string') {
559
+ headers[key] = current ? `${current}, ${val}` : val
560
+ continue
561
+ }
562
+
563
+ for (let i = 0; i < val.length; i++) {
564
+ headers[key] = headers[key] ? `${headers[key]}, ${val[i]}` : val[i]
456
565
  }
457
566
  }
458
567
 
568
+ return headers
569
+ }
570
+
571
+ function removeUpgradeStreamListeners (stream) {
572
+ stream.off('response', onUpgradeResponse)
573
+ stream.off('error', onUpgradeStreamError)
574
+ stream.off('end', onUpgradeStreamEnd)
575
+ stream.off('timeout', onUpgradeStreamTimeout)
576
+ stream.off('error', noop)
577
+ }
578
+
579
+ function releaseUpgradeStream (stream) {
580
+ if (stream == null) {
581
+ return
582
+ }
583
+
584
+ const state = stream[kRequestStreamState]
585
+ if (state == null) {
586
+ return
587
+ }
588
+
589
+ const { request } = state
590
+
591
+ if (request[kRequestStream] === stream) {
592
+ detachRequestFromStream(request)
593
+ }
594
+
595
+ removeUpgradeStreamListeners(stream)
596
+
597
+ if (!stream.destroyed && !stream.closed) {
598
+ stream.once('error', noop)
599
+ }
600
+ }
601
+
602
+ function failUpgradeStream (state, err) {
603
+ if (state == null) {
604
+ return
605
+ }
606
+
607
+ const { request } = state
608
+ if (state.responseReceived || request.aborted || request.completed) {
609
+ return
610
+ }
611
+
612
+ releaseUpgradeStream(state.stream)
613
+ state.abort(err, true)
614
+ }
615
+
616
+ function onUpgradeStreamError () {
617
+ const state = this[kRequestStreamState]
618
+
619
+ if (typeof this.rstCode === 'number' && this.rstCode !== 0) {
620
+ failUpgradeStream(state, new InformationalError(`HTTP/2: "stream error" received - code ${this.rstCode}`))
621
+ } else {
622
+ failUpgradeStream(state, new InformationalError('HTTP/2: stream errored before response headers'))
623
+ }
624
+ }
625
+
626
+ function onUpgradeStreamEnd () {
627
+ failUpgradeStream(this[kRequestStreamState], new InformationalError('HTTP/2: stream half-closed (remote)'))
628
+ }
629
+
630
+ function onUpgradeStreamTimeout () {
631
+ const state = this[kRequestStreamState]
632
+ failUpgradeStream(state, new InformationalError(`HTTP/2: "stream timeout after ${state.requestTimeout}"`))
633
+ }
634
+
635
+ function onUpgradeResponse (headers, _flags) {
636
+ const stream = this
637
+ const state = stream[kRequestStreamState]
638
+ const { request } = state
639
+
640
+ state.responseReceived = true
641
+
642
+ const statusCode = headers[HTTP2_HEADER_STATUS]
643
+ delete headers[HTTP2_HEADER_STATUS]
644
+
645
+ request.onRequestUpgrade(statusCode, headers, stream)
646
+
647
+ if (request.aborted || request.completed) {
648
+ return
649
+ }
650
+
651
+ removeUpgradeStreamListeners(stream)
652
+ detachRequestFromStream(request)
653
+ state.finalizeRequest()
654
+ }
655
+
656
+ function setupUpgradeStream (stream, state) {
657
+ const { request, requestTimeout, session } = state
658
+
659
+ stream[kHTTP2Stream] = true
660
+ stream[kHTTP2Session] = session
661
+ stream[kRequestStreamState] = state
662
+ state.stream = stream
663
+
664
+ bindRequestToStream(request, stream, releaseUpgradeStream)
665
+ stream.once('response', onUpgradeResponse)
666
+ stream.on('error', onUpgradeStreamError)
667
+ stream.once('end', onUpgradeStreamEnd)
668
+ stream.on('timeout', onUpgradeStreamTimeout)
669
+ stream.once('close', onUpgradeStreamClose)
670
+
671
+ ++session[kOpenStreams]
672
+ stream.setTimeout(requestTimeout)
673
+ }
674
+
675
+ function writeH2 (client, request) {
676
+ const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
677
+ const session = client[kHTTP2Session]
678
+ const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
679
+ let { body } = request
680
+
681
+ if (upgrade != null && upgrade !== 'websocket') {
682
+ util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
683
+ return false
684
+ }
685
+
686
+ const headers = buildRequestHeaders(reqHeaders)
687
+
459
688
  /** @type {import('node:http2').ClientHttp2Stream} */
460
689
  let stream = null
461
690
 
462
- const { hostname, port } = client[kUrl]
463
-
464
- headers[HTTP2_HEADER_AUTHORITY] = host || `${hostname}${port ? `:${port}` : ''}`
691
+ headers[HTTP2_HEADER_AUTHORITY] = host || client[kHostAuthority]
465
692
  headers[HTTP2_HEADER_METHOD] = method
466
693
 
467
- const abort = (err) => {
694
+ let requestFinalized = false
695
+ const finalizeRequest = (resetPendingIdx = false) => {
696
+ if (requestFinalized) {
697
+ return
698
+ }
699
+
700
+ requestFinalized = true
701
+ client[kQueue][client[kRunningIdx]++] = null
702
+
703
+ if (resetPendingIdx) {
704
+ client[kPendingIdx] = client[kRunningIdx]
705
+ }
706
+
707
+ client[kResume]()
708
+ }
709
+
710
+ const abort = (err, resetPendingIdx = false) => {
468
711
  if (request.aborted || request.completed) {
469
712
  return
470
713
  }
@@ -474,16 +717,14 @@ function writeH2 (client, request) {
474
717
  util.errorRequest(client, request, err)
475
718
 
476
719
  if (stream != null) {
477
- // Some chunks might still come after abort,
478
- // let's ignore them
479
- stream.removeAllListeners('data')
720
+ clearRequestStream(request)
480
721
 
481
722
  // On Abort, we close the stream to send RST_STREAM frame
482
723
  stream.close()
483
724
 
484
725
  // We move the running index to the next request
485
726
  client[kOnError](err)
486
- client[kResume]()
727
+ finalizeRequest(resetPendingIdx)
487
728
  }
488
729
 
489
730
  // We do not destroy the socket as we can continue using the session
@@ -491,10 +732,30 @@ function writeH2 (client, request) {
491
732
  util.destroy(body, err)
492
733
  }
493
734
 
735
+ const requestStream = (headers, options) => {
736
+ try {
737
+ return session.request(headers, options)
738
+ } catch (err) {
739
+ if (err?.code !== 'ERR_HTTP2_INVALID_CONNECTION_HEADERS') {
740
+ throw err
741
+ }
742
+
743
+ const wrappedErr = new InformationalError(err.message, { cause: err })
744
+ session[kError] = wrappedErr
745
+ session[kSocket][kError] = wrappedErr
746
+
747
+ session.destroy(wrappedErr)
748
+ util.destroy(session[kSocket], wrappedErr)
749
+ abort(wrappedErr)
750
+
751
+ return null
752
+ }
753
+ }
754
+
494
755
  try {
495
756
  // We are already connected, streams are pending.
496
757
  // We can call on connect, and wait for abort
497
- request.onConnect(abort)
758
+ request.onRequestStart(abort, null)
498
759
  } catch (err) {
499
760
  util.errorRequest(client, request, err)
500
761
  }
@@ -506,6 +767,16 @@ function writeH2 (client, request) {
506
767
  if (upgrade || method === 'CONNECT') {
507
768
  session.ref()
508
769
 
770
+ const upgradeState = {
771
+ abort,
772
+ finalizeRequest,
773
+ request,
774
+ requestTimeout,
775
+ responseReceived: false,
776
+ session,
777
+ stream: null
778
+ }
779
+
509
780
  if (upgrade === 'websocket') {
510
781
  // We cannot upgrade to websocket if extended CONNECT protocol is not supported
511
782
  if (session[kEnableConnectProtocol] === false) {
@@ -528,33 +799,12 @@ function writeH2 (client, request) {
528
799
  headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
529
800
  }
530
801
 
531
- stream = session.request(headers, { endStream: false, signal })
532
- stream[kHTTP2Stream] = true
533
-
534
- stream.once('response', (headers, _flags) => {
535
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
536
-
537
- request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
538
-
539
- ++session[kOpenStreams]
540
- client[kQueue][client[kRunningIdx]++] = null
541
- })
542
-
543
- stream.on('error', () => {
544
- if (stream.rstCode === NGHTTP2_REFUSED_STREAM || stream.rstCode === NGHTTP2_CANCEL) {
545
- // NGHTTP2_REFUSED_STREAM (7) or NGHTTP2_CANCEL (8)
546
- // We do not treat those as errors as the server might
547
- // not support websockets and refuse the stream
548
- abort(new InformationalError(`HTTP/2: "stream error" received - code ${stream.rstCode}`))
549
- }
550
- })
551
-
552
- stream.once('close', () => {
553
- session[kOpenStreams] -= 1
554
- if (session[kOpenStreams] === 0) session.unref()
555
- })
556
-
557
- stream.setTimeout(requestTimeout)
802
+ stream = requestStream(headers, { endStream: false, signal })
803
+ if (stream == null) {
804
+ session.unref()
805
+ return false
806
+ }
807
+ setupUpgradeStream(stream, upgradeState)
558
808
  return true
559
809
  }
560
810
 
@@ -563,20 +813,12 @@ function writeH2 (client, request) {
563
813
  // will create a new stream. We trigger a request to create the stream and wait until
564
814
  // `ready` event is triggered
565
815
  // We disabled endStream to allow the user to write to the stream
566
- stream = session.request(headers, { endStream: false, signal })
567
- stream[kHTTP2Stream] = true
568
- stream.on('response', headers => {
569
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
570
-
571
- request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
572
- ++session[kOpenStreams]
573
- client[kQueue][client[kRunningIdx]++] = null
574
- })
575
- stream.once('close', () => {
576
- session[kOpenStreams] -= 1
577
- if (session[kOpenStreams] === 0) session.unref()
578
- })
579
- stream.setTimeout(requestTimeout)
816
+ stream = requestStream(headers, { endStream: false, signal })
817
+ if (stream == null) {
818
+ session.unref()
819
+ return false
820
+ }
821
+ setupUpgradeStream(stream, upgradeState)
580
822
 
581
823
  return true
582
824
  }
@@ -598,7 +840,10 @@ function writeH2 (client, request) {
598
840
  const expectsPayload = (
599
841
  method === 'PUT' ||
600
842
  method === 'POST' ||
601
- method === 'PATCH'
843
+ method === 'PATCH' ||
844
+ method === 'QUERY' ||
845
+ method === 'PROPFIND' ||
846
+ method === 'PROPPATCH'
602
847
  )
603
848
 
604
849
  if (body && typeof body.read === 'function') {
@@ -622,7 +867,7 @@ function writeH2 (client, request) {
622
867
  contentLength = request.contentLength
623
868
  }
624
869
 
625
- if (!expectsPayload) {
870
+ if (contentLength === 0 && !expectsPayload) {
626
871
  // https://tools.ietf.org/html/rfc7230#section-3.3.2
627
872
  // A user agent SHOULD NOT send a Content-Length header field when
628
873
  // the request message does not contain a payload body and the method
@@ -658,185 +903,251 @@ function writeH2 (client, request) {
658
903
  }
659
904
 
660
905
  // TODO(metcoder95): add support for sending trailers
661
- const shouldEndStream = method === 'GET' || method === 'HEAD' || body === null
906
+ const shouldEndStream = body === null || contentLength === 0
907
+ const state = {
908
+ abort,
909
+ body,
910
+ client,
911
+ contentLength,
912
+ expectsPayload,
913
+ finalizeRequest,
914
+ request,
915
+ requestTimeout,
916
+ responseReceived: false,
917
+ session,
918
+ stream: null
919
+ }
920
+
662
921
  if (expectContinue) {
663
922
  headers[HTTP2_HEADER_EXPECT] = '100-continue'
664
- stream = session.request(headers, { endStream: shouldEndStream, signal })
665
- stream[kHTTP2Stream] = true
666
-
667
- stream.once('continue', writeBodyH2)
668
- } else {
669
- stream = session.request(headers, {
670
- endStream: shouldEndStream,
671
- signal
672
- })
673
- stream[kHTTP2Stream] = true
923
+ }
674
924
 
675
- writeBodyH2()
925
+ stream = requestStream(headers, { endStream: shouldEndStream, signal })
926
+ if (stream == null) {
927
+ return false
676
928
  }
929
+ stream[kHTTP2Stream] = true
930
+ stream[kRequestStreamState] = state
931
+ state.stream = stream
932
+ bindRequestToStream(request, stream, null)
677
933
 
678
934
  // Increment counter as we have new streams open
679
935
  ++session[kOpenStreams]
680
936
  stream.setTimeout(requestTimeout)
681
937
 
682
- // Track whether we received a response (headers)
683
- let responseReceived = false
684
-
685
- stream.once('response', headers => {
686
- const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
687
- request.onResponseStarted()
688
- responseReceived = true
689
-
690
- // Due to the stream nature, it is possible we face a race condition
691
- // where the stream has been assigned, but the request has been aborted
692
- // the request remains in-flight and headers hasn't been received yet
693
- // for those scenarios, best effort is to destroy the stream immediately
694
- // as there's no value to keep it open.
695
- if (request.aborted) {
696
- stream.removeAllListeners('data')
697
- return
698
- }
938
+ stream[kHTTP2Session] = session
939
+ stream.once('close', onRequestStreamClose)
699
940
 
700
- if (request.onHeaders(Number(statusCode), parseH2Headers(realHeaders), stream.resume.bind(stream), '') === false) {
701
- stream.pause()
702
- }
941
+ bindRequestToStream(request, stream, releaseRequestStream)
942
+ if (expectContinue) {
943
+ stream.once('continue', writeBodyH2)
944
+ }
945
+ stream.once('response', onResponse)
946
+ stream.once('end', onEnd)
947
+ stream.once('error', onError)
948
+ stream.once('frameError', onFrameError)
949
+ stream.on('aborted', onAborted)
950
+ stream.on('timeout', onTimeout)
951
+ stream.once('trailers', onTrailers)
952
+
953
+ if (!expectContinue) {
954
+ writeBodyH2.call(stream)
955
+ }
703
956
 
704
- stream.on('data', (chunk) => {
705
- if (request.aborted || request.completed) {
706
- return
707
- }
957
+ return true
958
+ }
708
959
 
709
- if (request.onData(chunk) === false) {
710
- stream.pause()
711
- }
712
- })
713
- })
960
+ function removeRequestStreamListeners (stream) {
961
+ stream.off('error', noop)
962
+ stream.off('continue', writeBodyH2)
963
+ stream.off('response', onResponse)
964
+ stream.off('end', onEnd)
965
+ stream.off('error', onError)
966
+ stream.off('frameError', onFrameError)
967
+ stream.off('aborted', onAborted)
968
+ stream.off('timeout', onTimeout)
969
+ stream.off('trailers', onTrailers)
970
+ stream.off('data', onData)
971
+ }
714
972
 
715
- stream.once('end', () => {
716
- stream.removeAllListeners('data')
717
- // If we received a response, this is a normal completion
718
- if (responseReceived) {
719
- if (!request.aborted && !request.completed) {
720
- request.onComplete({})
721
- }
973
+ function releaseRequestStream (stream) {
974
+ if (stream == null) {
975
+ return
976
+ }
722
977
 
723
- client[kQueue][client[kRunningIdx]++] = null
724
- client[kResume]()
725
- } else {
726
- // Stream ended without receiving a response - this is an error
727
- // (e.g., server destroyed the stream before sending headers)
728
- abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
729
- client[kQueue][client[kRunningIdx]++] = null
730
- client[kPendingIdx] = client[kRunningIdx]
731
- client[kResume]()
732
- }
733
- })
978
+ const state = stream[kRequestStreamState]
979
+ if (state == null) {
980
+ return
981
+ }
734
982
 
735
- stream.once('close', () => {
736
- stream.removeAllListeners('data')
737
- session[kOpenStreams] -= 1
738
- if (session[kOpenStreams] === 0) {
739
- session.unref()
740
- }
741
- })
983
+ const { request } = state
742
984
 
743
- stream.once('error', function (err) {
744
- stream.removeAllListeners('data')
745
- abort(err)
746
- })
985
+ if (request[kRequestStream] === stream) {
986
+ detachRequestFromStream(request)
987
+ }
747
988
 
748
- stream.once('frameError', (type, code) => {
749
- stream.removeAllListeners('data')
750
- abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
751
- })
989
+ removeRequestStreamListeners(stream)
752
990
 
753
- stream.on('aborted', () => {
754
- stream.removeAllListeners('data')
755
- })
991
+ if (!stream.destroyed && !stream.closed) {
992
+ stream.once('error', noop)
993
+ }
994
+ }
756
995
 
757
- stream.on('timeout', () => {
758
- const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
759
- stream.removeAllListeners('data')
760
- session[kOpenStreams] -= 1
996
+ function onData (chunk) {
997
+ const stream = this
998
+ const { request } = stream[kRequestStreamState]
761
999
 
762
- if (session[kOpenStreams] === 0) {
763
- session.unref()
764
- }
1000
+ if (request.aborted || request.completed) {
1001
+ return
1002
+ }
765
1003
 
766
- abort(err)
767
- })
1004
+ if (request.onResponseData(chunk) === false) {
1005
+ stream.pause()
1006
+ }
1007
+ }
768
1008
 
769
- stream.once('trailers', trailers => {
770
- if (request.aborted || request.completed) {
771
- return
1009
+ function onResponse (headers) {
1010
+ const stream = this
1011
+ const state = stream[kRequestStreamState]
1012
+ const { request } = state
1013
+
1014
+ stream.off('response', onResponse)
1015
+
1016
+ const statusCode = headers[HTTP2_HEADER_STATUS]
1017
+ delete headers[HTTP2_HEADER_STATUS]
1018
+ request.onResponseStarted()
1019
+ state.responseReceived = true
1020
+
1021
+ // Due to the stream nature, it is possible we face a race condition
1022
+ // where the stream has been assigned, but the request has been aborted
1023
+ // the request remains in-flight and headers hasn't been received yet
1024
+ // for those scenarios, best effort is to destroy the stream immediately
1025
+ // as there's no value to keep it open.
1026
+ if (request.aborted) {
1027
+ releaseRequestStream(stream)
1028
+ return
1029
+ }
1030
+
1031
+ if (request.onResponseStart(Number(statusCode), headers, stream.resume.bind(stream), '') === false) {
1032
+ stream.pause()
1033
+ }
1034
+
1035
+ stream.on('data', onData)
1036
+ }
1037
+
1038
+ function onEnd () {
1039
+ const stream = this
1040
+ const state = stream[kRequestStreamState]
1041
+ const { request } = state
1042
+
1043
+ stream.off('end', onEnd)
1044
+
1045
+ releaseRequestStream(stream)
1046
+ // If we received a response, this is a normal completion
1047
+ if (state.responseReceived) {
1048
+ if (!request.aborted && !request.completed) {
1049
+ request.onResponseEnd({})
772
1050
  }
773
1051
 
774
- stream.removeAllListeners('data')
775
- request.onComplete(trailers)
776
- })
1052
+ state.finalizeRequest()
1053
+ } else {
1054
+ // Stream ended without receiving a response - this is an error
1055
+ // (e.g., server destroyed the stream before sending headers)
1056
+ state.abort(new InformationalError('HTTP/2: stream half-closed (remote)'), true)
1057
+ }
1058
+ }
777
1059
 
778
- return true
1060
+ function onError (err) {
1061
+ const stream = this
1062
+ const state = stream[kRequestStreamState]
779
1063
 
780
- function writeBodyH2 () {
781
- if (!body || contentLength === 0) {
782
- writeBuffer(
783
- abort,
784
- stream,
785
- null,
786
- client,
787
- request,
788
- client[kSocket],
789
- contentLength,
790
- expectsPayload
791
- )
792
- } else if (util.isBuffer(body)) {
793
- writeBuffer(
1064
+ stream.off('error', onError)
1065
+
1066
+ releaseRequestStream(stream)
1067
+ state.abort(err)
1068
+ }
1069
+
1070
+ function onFrameError (type, code) {
1071
+ const stream = this
1072
+ const state = stream[kRequestStreamState]
1073
+
1074
+ stream.off('frameError', onFrameError)
1075
+
1076
+ releaseRequestStream(stream)
1077
+ state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
1078
+ }
1079
+
1080
+ function onAborted () {
1081
+ this.off('data', onData)
1082
+ }
1083
+
1084
+ function onTimeout () {
1085
+ const stream = this
1086
+ const state = stream[kRequestStreamState]
1087
+
1088
+ releaseRequestStream(stream)
1089
+
1090
+ const err = new InformationalError(`HTTP/2: "stream timeout after ${state.requestTimeout}"`)
1091
+ state.abort(err)
1092
+ }
1093
+
1094
+ function onTrailers (trailers) {
1095
+ const stream = this
1096
+ const state = stream[kRequestStreamState]
1097
+ const { request } = state
1098
+
1099
+ stream.off('trailers', onTrailers)
1100
+
1101
+ if (request.aborted || request.completed) {
1102
+ return
1103
+ }
1104
+
1105
+ releaseRequestStream(stream)
1106
+ request.onResponseEnd(trailers)
1107
+ state.finalizeRequest()
1108
+ }
1109
+
1110
+ function writeBodyH2 () {
1111
+ const stream = this
1112
+ const state = stream[kRequestStreamState]
1113
+ const { abort, body, client, contentLength, expectsPayload, request } = state
1114
+
1115
+ if (!body || contentLength === 0) {
1116
+ writeBuffer(
1117
+ abort,
1118
+ stream,
1119
+ null,
1120
+ client,
1121
+ request,
1122
+ client[kSocket],
1123
+ contentLength,
1124
+ expectsPayload
1125
+ )
1126
+ } else if (util.isBuffer(body)) {
1127
+ writeBuffer(
1128
+ abort,
1129
+ stream,
1130
+ body,
1131
+ client,
1132
+ request,
1133
+ client[kSocket],
1134
+ contentLength,
1135
+ expectsPayload
1136
+ )
1137
+ } else if (util.isBlobLike(body)) {
1138
+ if (typeof body.stream === 'function') {
1139
+ writeIterable(
794
1140
  abort,
795
1141
  stream,
796
- body,
1142
+ body.stream(),
797
1143
  client,
798
1144
  request,
799
1145
  client[kSocket],
800
1146
  contentLength,
801
1147
  expectsPayload
802
1148
  )
803
- } else if (util.isBlobLike(body)) {
804
- if (typeof body.stream === 'function') {
805
- writeIterable(
806
- abort,
807
- stream,
808
- body.stream(),
809
- client,
810
- request,
811
- client[kSocket],
812
- contentLength,
813
- expectsPayload
814
- )
815
- } else {
816
- writeBlob(
817
- abort,
818
- stream,
819
- body,
820
- client,
821
- request,
822
- client[kSocket],
823
- contentLength,
824
- expectsPayload
825
- )
826
- }
827
- } else if (util.isStream(body)) {
828
- writeStream(
829
- abort,
830
- client[kSocket],
831
- expectsPayload,
832
- stream,
833
- body,
834
- client,
835
- request,
836
- contentLength
837
- )
838
- } else if (util.isIterable(body)) {
839
- writeIterable(
1149
+ } else {
1150
+ writeBlob(
840
1151
  abort,
841
1152
  stream,
842
1153
  body,
@@ -846,9 +1157,31 @@ function writeH2 (client, request) {
846
1157
  contentLength,
847
1158
  expectsPayload
848
1159
  )
849
- } else {
850
- assert(false)
851
1160
  }
1161
+ } else if (util.isStream(body)) {
1162
+ writeStream(
1163
+ abort,
1164
+ client[kSocket],
1165
+ expectsPayload,
1166
+ stream,
1167
+ body,
1168
+ client,
1169
+ request,
1170
+ contentLength
1171
+ )
1172
+ } else if (util.isIterable(body)) {
1173
+ writeIterable(
1174
+ abort,
1175
+ stream,
1176
+ body,
1177
+ client,
1178
+ request,
1179
+ client[kSocket],
1180
+ contentLength,
1181
+ expectsPayload
1182
+ )
1183
+ } else {
1184
+ assert(false)
852
1185
  }
853
1186
  }
854
1187
 
@@ -907,8 +1240,6 @@ function writeStream (abort, socket, expectsPayload, h2stream, body, client, req
907
1240
  }
908
1241
 
909
1242
  async function writeBlob (abort, h2stream, body, client, request, socket, contentLength, expectsPayload) {
910
- assert(contentLength === body.size, 'blob body must have content length')
911
-
912
1243
  try {
913
1244
  if (contentLength != null && contentLength !== body.size) {
914
1245
  throw new RequestContentLengthMismatchError()