@suveren/gateway 0.9.0 → 0.10.0

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 (221) hide show
  1. package/README.md +33 -1
  2. package/bin/suveren-gateway.js +203 -5
  3. package/content/integrations/erp.json +184 -0
  4. package/content/integrations/index.json +2 -1
  5. package/dist/control-plane/index.mjs +653 -179
  6. package/dist/mcp-server/http.mjs +466 -259
  7. package/dist/ui/assets/{index-CIiOCgjJ.js → index-B1qNl7zo.js} +1 -1
  8. package/dist/ui/index.html +1 -1
  9. package/lib/as-pairing.mjs +77 -0
  10. package/lib/config.mjs +48 -0
  11. package/node_modules/undici/LICENSE +21 -0
  12. package/node_modules/undici/README.md +741 -0
  13. package/node_modules/undici/docs/docs/api/Agent.md +84 -0
  14. package/node_modules/undici/docs/docs/api/BalancedPool.md +99 -0
  15. package/node_modules/undici/docs/docs/api/CacheStorage.md +30 -0
  16. package/node_modules/undici/docs/docs/api/CacheStore.md +164 -0
  17. package/node_modules/undici/docs/docs/api/Client.md +288 -0
  18. package/node_modules/undici/docs/docs/api/ClientStats.md +27 -0
  19. package/node_modules/undici/docs/docs/api/Connector.md +115 -0
  20. package/node_modules/undici/docs/docs/api/ContentType.md +57 -0
  21. package/node_modules/undici/docs/docs/api/Cookies.md +128 -0
  22. package/node_modules/undici/docs/docs/api/Debug.md +62 -0
  23. package/node_modules/undici/docs/docs/api/DiagnosticsChannel.md +317 -0
  24. package/node_modules/undici/docs/docs/api/Dispatcher.md +1393 -0
  25. package/node_modules/undici/docs/docs/api/EnvHttpProxyAgent.md +159 -0
  26. package/node_modules/undici/docs/docs/api/Errors.md +49 -0
  27. package/node_modules/undici/docs/docs/api/EventSource.md +45 -0
  28. package/node_modules/undici/docs/docs/api/Fetch.md +60 -0
  29. package/node_modules/undici/docs/docs/api/GlobalInstallation.md +139 -0
  30. package/node_modules/undici/docs/docs/api/H2CClient.md +263 -0
  31. package/node_modules/undici/docs/docs/api/MockAgent.md +603 -0
  32. package/node_modules/undici/docs/docs/api/MockCallHistory.md +197 -0
  33. package/node_modules/undici/docs/docs/api/MockCallHistoryLog.md +43 -0
  34. package/node_modules/undici/docs/docs/api/MockClient.md +81 -0
  35. package/node_modules/undici/docs/docs/api/MockErrors.md +12 -0
  36. package/node_modules/undici/docs/docs/api/MockPool.md +555 -0
  37. package/node_modules/undici/docs/docs/api/Pool.md +84 -0
  38. package/node_modules/undici/docs/docs/api/PoolStats.md +35 -0
  39. package/node_modules/undici/docs/docs/api/ProxyAgent.md +229 -0
  40. package/node_modules/undici/docs/docs/api/RedirectHandler.md +93 -0
  41. package/node_modules/undici/docs/docs/api/RetryAgent.md +50 -0
  42. package/node_modules/undici/docs/docs/api/RetryHandler.md +118 -0
  43. package/node_modules/undici/docs/docs/api/RoundRobinPool.md +145 -0
  44. package/node_modules/undici/docs/docs/api/SnapshotAgent.md +616 -0
  45. package/node_modules/undici/docs/docs/api/Socks5ProxyAgent.md +275 -0
  46. package/node_modules/undici/docs/docs/api/Util.md +25 -0
  47. package/node_modules/undici/docs/docs/api/WebSocket.md +141 -0
  48. package/node_modules/undici/docs/docs/api/api-lifecycle.md +91 -0
  49. package/node_modules/undici/docs/docs/best-practices/client-certificate.md +64 -0
  50. package/node_modules/undici/docs/docs/best-practices/crawling.md +58 -0
  51. package/node_modules/undici/docs/docs/best-practices/mocking-request.md +190 -0
  52. package/node_modules/undici/docs/docs/best-practices/proxy.md +127 -0
  53. package/node_modules/undici/docs/docs/best-practices/undici-vs-builtin-fetch.md +224 -0
  54. package/node_modules/undici/docs/docs/best-practices/writing-tests.md +63 -0
  55. package/node_modules/undici/index-fetch.js +65 -0
  56. package/node_modules/undici/index.d.ts +3 -0
  57. package/node_modules/undici/index.js +234 -0
  58. package/node_modules/undici/lib/api/abort-signal.js +59 -0
  59. package/node_modules/undici/lib/api/api-connect.js +110 -0
  60. package/node_modules/undici/lib/api/api-pipeline.js +252 -0
  61. package/node_modules/undici/lib/api/api-request.js +214 -0
  62. package/node_modules/undici/lib/api/api-stream.js +209 -0
  63. package/node_modules/undici/lib/api/api-upgrade.js +111 -0
  64. package/node_modules/undici/lib/api/index.js +7 -0
  65. package/node_modules/undici/lib/api/readable.js +580 -0
  66. package/node_modules/undici/lib/cache/memory-cache-store.js +279 -0
  67. package/node_modules/undici/lib/cache/sqlite-cache-store.js +467 -0
  68. package/node_modules/undici/lib/core/connect.js +153 -0
  69. package/node_modules/undici/lib/core/constants.js +143 -0
  70. package/node_modules/undici/lib/core/diagnostics.js +227 -0
  71. package/node_modules/undici/lib/core/errors.js +477 -0
  72. package/node_modules/undici/lib/core/request.js +485 -0
  73. package/node_modules/undici/lib/core/socks5-client.js +422 -0
  74. package/node_modules/undici/lib/core/socks5-utils.js +212 -0
  75. package/node_modules/undici/lib/core/symbols.js +75 -0
  76. package/node_modules/undici/lib/core/tree.js +160 -0
  77. package/node_modules/undici/lib/core/util.js +992 -0
  78. package/node_modules/undici/lib/dispatcher/agent.js +158 -0
  79. package/node_modules/undici/lib/dispatcher/balanced-pool.js +221 -0
  80. package/node_modules/undici/lib/dispatcher/client-h1.js +1746 -0
  81. package/node_modules/undici/lib/dispatcher/client-h2.js +1060 -0
  82. package/node_modules/undici/lib/dispatcher/client.js +668 -0
  83. package/node_modules/undici/lib/dispatcher/dispatcher-base.js +184 -0
  84. package/node_modules/undici/lib/dispatcher/dispatcher.js +48 -0
  85. package/node_modules/undici/lib/dispatcher/env-http-proxy-agent.js +146 -0
  86. package/node_modules/undici/lib/dispatcher/fixed-queue.js +135 -0
  87. package/node_modules/undici/lib/dispatcher/h2c-client.js +51 -0
  88. package/node_modules/undici/lib/dispatcher/pool-base.js +214 -0
  89. package/node_modules/undici/lib/dispatcher/pool.js +118 -0
  90. package/node_modules/undici/lib/dispatcher/proxy-agent.js +319 -0
  91. package/node_modules/undici/lib/dispatcher/retry-agent.js +35 -0
  92. package/node_modules/undici/lib/dispatcher/round-robin-pool.js +137 -0
  93. package/node_modules/undici/lib/dispatcher/socks5-proxy-agent.js +260 -0
  94. package/node_modules/undici/lib/encoding/index.js +33 -0
  95. package/node_modules/undici/lib/global.js +59 -0
  96. package/node_modules/undici/lib/handler/cache-handler.js +776 -0
  97. package/node_modules/undici/lib/handler/cache-revalidation-handler.js +124 -0
  98. package/node_modules/undici/lib/handler/decorator-handler.js +67 -0
  99. package/node_modules/undici/lib/handler/deduplication-handler.js +460 -0
  100. package/node_modules/undici/lib/handler/redirect-handler.js +238 -0
  101. package/node_modules/undici/lib/handler/retry-handler.js +504 -0
  102. package/node_modules/undici/lib/handler/unwrap-handler.js +106 -0
  103. package/node_modules/undici/lib/handler/wrap-handler.js +105 -0
  104. package/node_modules/undici/lib/interceptor/cache.js +621 -0
  105. package/node_modules/undici/lib/interceptor/decompress.js +551 -0
  106. package/node_modules/undici/lib/interceptor/deduplicate.js +117 -0
  107. package/node_modules/undici/lib/interceptor/dns.js +571 -0
  108. package/node_modules/undici/lib/interceptor/dump.js +99 -0
  109. package/node_modules/undici/lib/interceptor/redirect.js +21 -0
  110. package/node_modules/undici/lib/interceptor/response-error.js +95 -0
  111. package/node_modules/undici/lib/interceptor/retry.js +19 -0
  112. package/node_modules/undici/lib/llhttp/.gitkeep +0 -0
  113. package/node_modules/undici/lib/llhttp/constants.d.ts +195 -0
  114. package/node_modules/undici/lib/llhttp/constants.js +531 -0
  115. package/node_modules/undici/lib/llhttp/llhttp-wasm.js +15 -0
  116. package/node_modules/undici/lib/llhttp/llhttp_simd-wasm.js +15 -0
  117. package/node_modules/undici/lib/llhttp/utils.d.ts +2 -0
  118. package/node_modules/undici/lib/llhttp/utils.js +12 -0
  119. package/node_modules/undici/lib/mock/mock-agent.js +232 -0
  120. package/node_modules/undici/lib/mock/mock-call-history.js +248 -0
  121. package/node_modules/undici/lib/mock/mock-client.js +68 -0
  122. package/node_modules/undici/lib/mock/mock-errors.js +29 -0
  123. package/node_modules/undici/lib/mock/mock-interceptor.js +209 -0
  124. package/node_modules/undici/lib/mock/mock-pool.js +68 -0
  125. package/node_modules/undici/lib/mock/mock-symbols.js +32 -0
  126. package/node_modules/undici/lib/mock/mock-utils.js +486 -0
  127. package/node_modules/undici/lib/mock/pending-interceptors-formatter.js +43 -0
  128. package/node_modules/undici/lib/mock/snapshot-agent.js +353 -0
  129. package/node_modules/undici/lib/mock/snapshot-recorder.js +588 -0
  130. package/node_modules/undici/lib/mock/snapshot-utils.js +158 -0
  131. package/node_modules/undici/lib/util/cache.js +676 -0
  132. package/node_modules/undici/lib/util/date.js +670 -0
  133. package/node_modules/undici/lib/util/promise.js +28 -0
  134. package/node_modules/undici/lib/util/runtime-features.js +124 -0
  135. package/node_modules/undici/lib/util/stats.js +32 -0
  136. package/node_modules/undici/lib/util/timers.js +425 -0
  137. package/node_modules/undici/lib/web/cache/cache.js +864 -0
  138. package/node_modules/undici/lib/web/cache/cachestorage.js +152 -0
  139. package/node_modules/undici/lib/web/cache/util.js +45 -0
  140. package/node_modules/undici/lib/web/cookies/constants.js +12 -0
  141. package/node_modules/undici/lib/web/cookies/index.js +199 -0
  142. package/node_modules/undici/lib/web/cookies/parse.js +314 -0
  143. package/node_modules/undici/lib/web/cookies/util.js +352 -0
  144. package/node_modules/undici/lib/web/eventsource/eventsource-stream.js +494 -0
  145. package/node_modules/undici/lib/web/eventsource/eventsource.js +501 -0
  146. package/node_modules/undici/lib/web/eventsource/util.js +29 -0
  147. package/node_modules/undici/lib/web/fetch/LICENSE +21 -0
  148. package/node_modules/undici/lib/web/fetch/body.js +509 -0
  149. package/node_modules/undici/lib/web/fetch/constants.js +131 -0
  150. package/node_modules/undici/lib/web/fetch/data-url.js +596 -0
  151. package/node_modules/undici/lib/web/fetch/formdata-parser.js +586 -0
  152. package/node_modules/undici/lib/web/fetch/formdata.js +259 -0
  153. package/node_modules/undici/lib/web/fetch/global.js +40 -0
  154. package/node_modules/undici/lib/web/fetch/headers.js +719 -0
  155. package/node_modules/undici/lib/web/fetch/index.js +2413 -0
  156. package/node_modules/undici/lib/web/fetch/request.js +1115 -0
  157. package/node_modules/undici/lib/web/fetch/response.js +641 -0
  158. package/node_modules/undici/lib/web/fetch/util.js +1522 -0
  159. package/node_modules/undici/lib/web/infra/index.js +229 -0
  160. package/node_modules/undici/lib/web/subresource-integrity/Readme.md +9 -0
  161. package/node_modules/undici/lib/web/subresource-integrity/subresource-integrity.js +307 -0
  162. package/node_modules/undici/lib/web/webidl/index.js +1006 -0
  163. package/node_modules/undici/lib/web/websocket/connection.js +339 -0
  164. package/node_modules/undici/lib/web/websocket/constants.js +126 -0
  165. package/node_modules/undici/lib/web/websocket/events.js +331 -0
  166. package/node_modules/undici/lib/web/websocket/frame.js +133 -0
  167. package/node_modules/undici/lib/web/websocket/permessage-deflate.js +105 -0
  168. package/node_modules/undici/lib/web/websocket/receiver.js +507 -0
  169. package/node_modules/undici/lib/web/websocket/sender.js +109 -0
  170. package/node_modules/undici/lib/web/websocket/stream/websocketerror.js +104 -0
  171. package/node_modules/undici/lib/web/websocket/stream/websocketstream.js +496 -0
  172. package/node_modules/undici/lib/web/websocket/util.js +347 -0
  173. package/node_modules/undici/lib/web/websocket/websocket.js +758 -0
  174. package/node_modules/undici/package.json +152 -0
  175. package/node_modules/undici/scripts/strip-comments.js +10 -0
  176. package/node_modules/undici/types/README.md +6 -0
  177. package/node_modules/undici/types/agent.d.ts +32 -0
  178. package/node_modules/undici/types/api.d.ts +43 -0
  179. package/node_modules/undici/types/balanced-pool.d.ts +30 -0
  180. package/node_modules/undici/types/cache-interceptor.d.ts +179 -0
  181. package/node_modules/undici/types/cache.d.ts +36 -0
  182. package/node_modules/undici/types/client-stats.d.ts +15 -0
  183. package/node_modules/undici/types/client.d.ts +139 -0
  184. package/node_modules/undici/types/connector.d.ts +36 -0
  185. package/node_modules/undici/types/content-type.d.ts +21 -0
  186. package/node_modules/undici/types/cookies.d.ts +30 -0
  187. package/node_modules/undici/types/diagnostics-channel.d.ts +74 -0
  188. package/node_modules/undici/types/dispatcher.d.ts +275 -0
  189. package/node_modules/undici/types/env-http-proxy-agent.d.ts +22 -0
  190. package/node_modules/undici/types/errors.d.ts +177 -0
  191. package/node_modules/undici/types/eventsource.d.ts +66 -0
  192. package/node_modules/undici/types/fetch.d.ts +231 -0
  193. package/node_modules/undici/types/formdata.d.ts +114 -0
  194. package/node_modules/undici/types/global-dispatcher.d.ts +9 -0
  195. package/node_modules/undici/types/global-origin.d.ts +7 -0
  196. package/node_modules/undici/types/h2c-client.d.ts +73 -0
  197. package/node_modules/undici/types/handlers.d.ts +14 -0
  198. package/node_modules/undici/types/header.d.ts +160 -0
  199. package/node_modules/undici/types/index.d.ts +91 -0
  200. package/node_modules/undici/types/interceptors.d.ts +82 -0
  201. package/node_modules/undici/types/mock-agent.d.ts +68 -0
  202. package/node_modules/undici/types/mock-call-history.d.ts +111 -0
  203. package/node_modules/undici/types/mock-client.d.ts +27 -0
  204. package/node_modules/undici/types/mock-errors.d.ts +12 -0
  205. package/node_modules/undici/types/mock-interceptor.d.ts +94 -0
  206. package/node_modules/undici/types/mock-pool.d.ts +27 -0
  207. package/node_modules/undici/types/patch.d.ts +29 -0
  208. package/node_modules/undici/types/pool-stats.d.ts +19 -0
  209. package/node_modules/undici/types/pool.d.ts +41 -0
  210. package/node_modules/undici/types/proxy-agent.d.ts +29 -0
  211. package/node_modules/undici/types/readable.d.ts +68 -0
  212. package/node_modules/undici/types/retry-agent.d.ts +8 -0
  213. package/node_modules/undici/types/retry-handler.d.ts +125 -0
  214. package/node_modules/undici/types/round-robin-pool.d.ts +41 -0
  215. package/node_modules/undici/types/snapshot-agent.d.ts +109 -0
  216. package/node_modules/undici/types/socks5-proxy-agent.d.ts +25 -0
  217. package/node_modules/undici/types/util.d.ts +18 -0
  218. package/node_modules/undici/types/utility.d.ts +7 -0
  219. package/node_modules/undici/types/webidl.d.ts +347 -0
  220. package/node_modules/undici/types/websocket.d.ts +188 -0
  221. package/package.json +4 -2
@@ -0,0 +1,504 @@
1
+ 'use strict'
2
+ const assert = require('node:assert')
3
+
4
+ const { kRetryHandlerDefaultRetry } = require('../core/symbols')
5
+ const { RequestRetryError } = require('../core/errors')
6
+ const WrapHandler = require('./wrap-handler')
7
+ const {
8
+ isDisturbed,
9
+ parseRangeHeader,
10
+ wrapRequestBody
11
+ } = require('../core/util')
12
+
13
+ function calculateRetryAfterHeader (retryAfter) {
14
+ const retryTime = new Date(retryAfter).getTime()
15
+ return isNaN(retryTime) ? 0 : retryTime - Date.now()
16
+ }
17
+
18
+ function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
19
+ const contentLength = headers['content-length']
20
+ if (contentLength == null) {
21
+ return
22
+ }
23
+
24
+ if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
25
+ return
26
+ }
27
+
28
+ const length = Number(contentLength)
29
+ const expectedLength = range.end - range.start + 1
30
+ if (!Number.isFinite(length) || length !== expectedLength) {
31
+ throw new RequestRetryError('Content-Length mismatch', statusCode, {
32
+ headers,
33
+ data: { count: retryCount }
34
+ })
35
+ }
36
+ }
37
+
38
+ // A stable controller handed to the downstream handler for the lifetime of the
39
+ // request. Each transparent retry/resume is a separate dispatch with its own
40
+ // connection controller. The proxy always forwards to the active connection
41
+ // while preserving a downstream pause across controller replacement.
42
+ class RetryController {
43
+ #paused = false
44
+ #target = null
45
+
46
+ set target (target) {
47
+ this.#target = target
48
+ if (this.#paused) {
49
+ target?.pause()
50
+ }
51
+ }
52
+
53
+ get target () { return this.#target }
54
+
55
+ pause () {
56
+ this.#paused = true
57
+ this.#target?.pause()
58
+ }
59
+
60
+ resume () {
61
+ this.#paused = false
62
+ this.#target?.resume()
63
+ }
64
+
65
+ abort (reason) {
66
+ this.#target?.abort(reason)
67
+ }
68
+
69
+ get paused () { return this.#paused || (this.#target?.paused ?? false) }
70
+ get aborted () { return this.#target?.aborted ?? false }
71
+ get reason () { return this.#target?.reason ?? null }
72
+ get rawHeaders () { return this.#target?.rawHeaders ?? null }
73
+ set rawHeaders (value) {
74
+ if (this.#target) {
75
+ this.#target.rawHeaders = value
76
+ }
77
+ }
78
+
79
+ get rawTrailers () { return this.#target?.rawTrailers ?? null }
80
+ set rawTrailers (value) {
81
+ if (this.#target) {
82
+ this.#target.rawTrailers = value
83
+ }
84
+ }
85
+ }
86
+
87
+ class RetryHandler {
88
+ constructor (opts, { dispatch, handler }) {
89
+ const { retryOptions, ...dispatchOpts } = opts
90
+ const {
91
+ // Retry scoped
92
+ retry: retryFn,
93
+ maxRetries,
94
+ maxTimeout,
95
+ minTimeout,
96
+ timeoutFactor,
97
+ // Response scoped
98
+ methods,
99
+ errorCodes,
100
+ retryAfter,
101
+ statusCodes,
102
+ throwOnError
103
+ } = retryOptions ?? {}
104
+
105
+ this.error = null
106
+ this.dispatch = dispatch
107
+ this.handler = WrapHandler.wrap(handler)
108
+ this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
109
+ this.retryOpts = {
110
+ throwOnError: throwOnError ?? true,
111
+ retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
112
+ retryAfter: retryAfter ?? true,
113
+ maxTimeout: maxTimeout ?? 30 * 1000, // 30s,
114
+ minTimeout: minTimeout ?? 500, // .5s
115
+ timeoutFactor: timeoutFactor ?? 2,
116
+ maxRetries: maxRetries ?? 5,
117
+ // What errors we should retry
118
+ methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
119
+ // Indicates which errors to retry
120
+ statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
121
+ // List of errors to retry
122
+ errorCodes: errorCodes ?? [
123
+ 'ECONNRESET',
124
+ 'ECONNREFUSED',
125
+ 'ENOTFOUND',
126
+ 'ENETDOWN',
127
+ 'ENETUNREACH',
128
+ 'EHOSTDOWN',
129
+ 'EHOSTUNREACH',
130
+ 'EPIPE',
131
+ 'UND_ERR_SOCKET'
132
+ ]
133
+ }
134
+
135
+ this.retryCount = 0
136
+ this.retryCountCheckpoint = 0
137
+ this.headersSent = false
138
+ this.start = 0
139
+ this.end = null
140
+ this.etag = null
141
+ this.controllerProxy = new RetryController()
142
+ }
143
+
144
+ onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
145
+ if (this.retryOpts.throwOnError) {
146
+ // Preserve old behavior for status codes that are not eligible for retry
147
+ if (this.retryOpts.statusCodes.includes(statusCode) === false) {
148
+ if (this.headersSent) {
149
+ // The downstream handler already received the response from an
150
+ // earlier attempt. Forwarding this response would replace the
151
+ // downstream body and leave the original body pending forever.
152
+ this.handler.onResponseError?.(this.controllerProxy, err)
153
+ } else {
154
+ this.headersSent = true
155
+ this.checkpointResponseEnd(headers)
156
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
157
+ }
158
+ } else {
159
+ this.error = err
160
+ }
161
+
162
+ return
163
+ }
164
+
165
+ if (isDisturbed(this.opts.body)) {
166
+ this.headersSent = true
167
+ this.checkpointResponseEnd(headers)
168
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
169
+ return
170
+ }
171
+
172
+ function shouldRetry (passedErr) {
173
+ if (passedErr) {
174
+ if (this.headersSent) {
175
+ // The downstream handler already received the response from an
176
+ // earlier attempt. Forwarding this response would replace the
177
+ // downstream body and leave the original body pending forever.
178
+ this.handler.onResponseError?.(this.controllerProxy, passedErr)
179
+ } else {
180
+ this.headersSent = true
181
+ this.checkpointResponseEnd(headers)
182
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
183
+ }
184
+ controller.resume()
185
+ return
186
+ }
187
+
188
+ this.error = err
189
+ controller.resume()
190
+ }
191
+
192
+ controller.pause()
193
+ this.retryOpts.retry(
194
+ err,
195
+ {
196
+ state: { counter: this.retryCount },
197
+ opts: { retryOptions: this.retryOpts, ...this.opts }
198
+ },
199
+ shouldRetry.bind(this)
200
+ )
201
+ }
202
+
203
+ checkpointResponseEnd (headers) {
204
+ if (this.end == null && this.opts.method !== 'HEAD') {
205
+ const contentLength = headers['content-length']
206
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
207
+
208
+ assert(
209
+ this.end == null || Number.isFinite(this.end),
210
+ 'invalid content-length'
211
+ )
212
+
213
+ this.resume = this.end != null
214
+ }
215
+ }
216
+
217
+ onRequestStart (controller, context) {
218
+ this.controllerProxy.target = controller
219
+ if (!this.headersSent) {
220
+ this.handler.onRequestStart?.(this.controllerProxy, context)
221
+ }
222
+ }
223
+
224
+ onRequestUpgrade (_controller, statusCode, headers, socket) {
225
+ this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket)
226
+ }
227
+
228
+ static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
229
+ const { statusCode, code, headers } = err
230
+ const { method, retryOptions } = opts
231
+ const {
232
+ maxRetries,
233
+ minTimeout,
234
+ maxTimeout,
235
+ timeoutFactor,
236
+ statusCodes,
237
+ errorCodes,
238
+ methods
239
+ } = retryOptions
240
+ const { counter } = state
241
+
242
+ // Any code that is not a Undici's originated and allowed to retry
243
+ if (code && code !== 'UND_ERR_REQ_RETRY' && !errorCodes.includes(code)) {
244
+ cb(err)
245
+ return
246
+ }
247
+
248
+ // If a set of method are provided and the current method is not in the list
249
+ if (Array.isArray(methods) && !methods.includes(method)) {
250
+ cb(err)
251
+ return
252
+ }
253
+
254
+ // If a set of status code are provided and the current status code is not in the list
255
+ if (
256
+ statusCode != null &&
257
+ Array.isArray(statusCodes) &&
258
+ !statusCodes.includes(statusCode)
259
+ ) {
260
+ cb(err)
261
+ return
262
+ }
263
+
264
+ // If we reached the max number of retries
265
+ if (counter > maxRetries) {
266
+ cb(err)
267
+ return
268
+ }
269
+
270
+ let retryAfterHeader = headers?.['retry-after']
271
+ if (retryAfterHeader) {
272
+ retryAfterHeader = Number(retryAfterHeader)
273
+ retryAfterHeader = Number.isNaN(retryAfterHeader)
274
+ ? calculateRetryAfterHeader(headers['retry-after'])
275
+ : retryAfterHeader * 1e3 // Retry-After is in seconds
276
+ }
277
+
278
+ const retryTimeout =
279
+ retryAfterHeader > 0
280
+ ? Math.min(retryAfterHeader, maxTimeout)
281
+ : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout)
282
+
283
+ setTimeout(() => cb(null), retryTimeout)
284
+ }
285
+
286
+ onResponseStart (controller, statusCode, headers, statusMessage) {
287
+ this.error = null
288
+ this.retryCount += 1
289
+
290
+ if (statusCode >= 300) {
291
+ const err = new RequestRetryError('Request failed', statusCode, {
292
+ headers,
293
+ data: {
294
+ count: this.retryCount
295
+ }
296
+ })
297
+
298
+ this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
299
+ return
300
+ }
301
+
302
+ // Checkpoint for resume from where we left it
303
+ if (this.headersSent) {
304
+ // Only Partial Content 206 supposed to provide Content-Range,
305
+ // any other status code that partially consumed the payload
306
+ // should not be retried because it would result in downstream
307
+ // wrongly concatenate multiple responses.
308
+ if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
309
+ throw new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
310
+ headers,
311
+ data: { count: this.retryCount }
312
+ })
313
+ }
314
+
315
+ const contentRange = parseRangeHeader(headers['content-range'])
316
+ // If no content range
317
+ if (!contentRange) {
318
+ // We always throw here as we want to indicate that we entred unexpected path
319
+ throw new RequestRetryError('Content-Range mismatch', statusCode, {
320
+ headers,
321
+ data: { count: this.retryCount }
322
+ })
323
+ }
324
+
325
+ // Let's start with a weak etag check
326
+ if (this.etag != null && this.etag !== headers.etag) {
327
+ // We always throw here as we want to indicate that we entred unexpected path
328
+ throw new RequestRetryError('ETag mismatch', statusCode, {
329
+ headers,
330
+ data: { count: this.retryCount }
331
+ })
332
+ }
333
+
334
+ validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)
335
+
336
+ const { start, size, end = size ? size - 1 : null } = contentRange
337
+
338
+ if (this.start !== start || (this.end != null && this.end !== end)) {
339
+ throw new RequestRetryError('Content-Range mismatch', statusCode, {
340
+ headers,
341
+ data: { count: this.retryCount }
342
+ })
343
+ }
344
+
345
+ return
346
+ }
347
+
348
+ if (this.end == null) {
349
+ if (statusCode === 206) {
350
+ // First time we receive 206
351
+ const range = parseRangeHeader(headers['content-range'])
352
+
353
+ if (range == null) {
354
+ this.headersSent = true
355
+ this.handler.onResponseStart?.(
356
+ this.controllerProxy,
357
+ statusCode,
358
+ headers,
359
+ statusMessage
360
+ )
361
+ return
362
+ }
363
+
364
+ validatePartialResponseContentLength(headers, range, statusCode, this.retryCount)
365
+
366
+ const { start, size, end = size ? size - 1 : null } = range
367
+ assert(
368
+ start != null && Number.isFinite(start),
369
+ 'content-range mismatch'
370
+ )
371
+ assert(end != null && Number.isFinite(end), 'invalid content-length')
372
+
373
+ this.start = start
374
+ this.end = end
375
+ }
376
+
377
+ // We make our best to checkpoint the body for further range headers
378
+ if (this.end == null) {
379
+ const contentLength = headers['content-length']
380
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
381
+ }
382
+
383
+ assert(Number.isFinite(this.start))
384
+ assert(
385
+ this.end == null || Number.isFinite(this.end),
386
+ 'invalid content-length'
387
+ )
388
+
389
+ this.resume = true
390
+ this.etag = headers.etag != null ? headers.etag : null
391
+
392
+ // Weak etags are not useful for comparison nor cache
393
+ // for instance not safe to assume if the response is byte-per-byte
394
+ // equal
395
+ if (
396
+ this.etag != null &&
397
+ this.etag[0] === 'W' &&
398
+ this.etag[1] === '/'
399
+ ) {
400
+ this.etag = null
401
+ }
402
+
403
+ this.headersSent = true
404
+ this.handler.onResponseStart?.(
405
+ this.controllerProxy,
406
+ statusCode,
407
+ headers,
408
+ statusMessage
409
+ )
410
+ } else {
411
+ throw new RequestRetryError('Request failed', statusCode, {
412
+ headers,
413
+ data: { count: this.retryCount }
414
+ })
415
+ }
416
+ }
417
+
418
+ onResponseData (_controller, chunk) {
419
+ if (this.error) {
420
+ return
421
+ }
422
+
423
+ this.start += chunk.length
424
+
425
+ this.handler.onResponseData?.(this.controllerProxy, chunk)
426
+ }
427
+
428
+ onResponseEnd (_controller, trailers) {
429
+ if (this.error && this.retryOpts.throwOnError) {
430
+ throw this.error
431
+ }
432
+
433
+ if (!this.error) {
434
+ this.retryCount = 0
435
+ return this.handler.onResponseEnd?.(this.controllerProxy, trailers)
436
+ }
437
+
438
+ this.retry()
439
+ }
440
+
441
+ retry () {
442
+ if (this.start !== 0) {
443
+ const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
444
+
445
+ // Weak etag check - weak etags will make comparison algorithms never match
446
+ if (this.etag != null) {
447
+ headers['if-match'] = this.etag
448
+ }
449
+
450
+ this.opts = {
451
+ ...this.opts,
452
+ headers: {
453
+ ...this.opts.headers,
454
+ ...headers
455
+ }
456
+ }
457
+ }
458
+
459
+ try {
460
+ this.retryCountCheckpoint = this.retryCount
461
+ this.dispatch(this.opts, this)
462
+ } catch (err) {
463
+ this.handler.onResponseError?.(this.controllerProxy, err)
464
+ }
465
+ }
466
+
467
+ onResponseError (controller, err) {
468
+ if (controller?.aborted || isDisturbed(this.opts.body) || (this.headersSent && !this.resume)) {
469
+ this.handler.onResponseError?.(this.controllerProxy, err)
470
+ return
471
+ }
472
+
473
+ function shouldRetry (returnedErr) {
474
+ if (!returnedErr) {
475
+ this.retry()
476
+ return
477
+ }
478
+
479
+ this.handler?.onResponseError?.(this.controllerProxy, returnedErr)
480
+ }
481
+
482
+ // We reconcile in case of a mix between network errors
483
+ // and server error response
484
+ if (this.retryCount - this.retryCountCheckpoint > 0) {
485
+ // We count the difference between the last checkpoint and the current retry count
486
+ this.retryCount =
487
+ this.retryCountCheckpoint +
488
+ (this.retryCount - this.retryCountCheckpoint)
489
+ } else {
490
+ this.retryCount += 1
491
+ }
492
+
493
+ this.retryOpts.retry(
494
+ err,
495
+ {
496
+ state: { counter: this.retryCount },
497
+ opts: { retryOptions: this.retryOpts, ...this.opts }
498
+ },
499
+ shouldRetry.bind(this)
500
+ )
501
+ }
502
+ }
503
+
504
+ module.exports = RetryHandler
@@ -0,0 +1,106 @@
1
+ 'use strict'
2
+
3
+ const { parseHeaders } = require('../core/util')
4
+ const { InvalidArgumentError } = require('../core/errors')
5
+
6
+ const kResume = Symbol('resume')
7
+
8
+ class UnwrapController {
9
+ #paused = false
10
+ #reason = null
11
+ #aborted = false
12
+ #abort
13
+
14
+ [kResume] = null
15
+
16
+ rawHeaders = null
17
+ rawTrailers = null
18
+
19
+ constructor (abort) {
20
+ this.#abort = abort
21
+ }
22
+
23
+ pause () {
24
+ this.#paused = true
25
+ }
26
+
27
+ resume () {
28
+ if (this.#paused) {
29
+ this.#paused = false
30
+ this[kResume]?.()
31
+ }
32
+ }
33
+
34
+ abort (reason) {
35
+ if (!this.#aborted) {
36
+ this.#aborted = true
37
+ this.#reason = reason
38
+ this.#abort(reason)
39
+ }
40
+ }
41
+
42
+ get aborted () {
43
+ return this.#aborted
44
+ }
45
+
46
+ get reason () {
47
+ return this.#reason
48
+ }
49
+
50
+ get paused () {
51
+ return this.#paused
52
+ }
53
+ }
54
+
55
+ module.exports = class UnwrapHandler {
56
+ #handler
57
+ #controller
58
+
59
+ constructor (handler) {
60
+ this.#handler = handler
61
+ }
62
+
63
+ static unwrap (handler) {
64
+ // TODO (fix): More checks...
65
+ return !handler.onRequestStart ? handler : new UnwrapHandler(handler)
66
+ }
67
+
68
+ onConnect (abort, context) {
69
+ this.#controller = new UnwrapController(abort)
70
+ this.#handler.onRequestStart?.(this.#controller, context)
71
+ }
72
+
73
+ onResponseStarted () {
74
+ return this.#handler.onResponseStarted?.()
75
+ }
76
+
77
+ onUpgrade (statusCode, rawHeaders, socket) {
78
+ this.#controller.rawHeaders = rawHeaders
79
+ this.#handler.onRequestUpgrade?.(this.#controller, statusCode, parseHeaders(rawHeaders), socket)
80
+ }
81
+
82
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
83
+ this.#controller[kResume] = resume
84
+ this.#controller.rawHeaders = rawHeaders
85
+ this.#handler.onResponseStart?.(this.#controller, statusCode, parseHeaders(rawHeaders), statusMessage)
86
+ return !this.#controller.paused
87
+ }
88
+
89
+ onData (data) {
90
+ this.#handler.onResponseData?.(this.#controller, data)
91
+ return !this.#controller.paused
92
+ }
93
+
94
+ onComplete (rawTrailers) {
95
+ this.#controller.rawTrailers = rawTrailers
96
+ this.#handler.onResponseEnd?.(this.#controller, parseHeaders(rawTrailers))
97
+ }
98
+
99
+ onError (err) {
100
+ if (!this.#handler.onResponseError) {
101
+ throw new InvalidArgumentError('invalid onError method')
102
+ }
103
+
104
+ this.#handler.onResponseError?.(this.#controller, err)
105
+ }
106
+ }
@@ -0,0 +1,105 @@
1
+ 'use strict'
2
+
3
+ const { InvalidArgumentError } = require('../core/errors')
4
+
5
+ module.exports = class WrapHandler {
6
+ #handler
7
+
8
+ constructor (handler) {
9
+ this.#handler = handler
10
+ }
11
+
12
+ static wrap (handler) {
13
+ // TODO (fix): More checks...
14
+ return handler.onRequestStart ? handler : new WrapHandler(handler)
15
+ }
16
+
17
+ // Unwrap Interface
18
+
19
+ onConnect (abort, context) {
20
+ return this.#handler.onConnect?.(abort, context)
21
+ }
22
+
23
+ onResponseStarted () {
24
+ return this.#handler.onResponseStarted?.()
25
+ }
26
+
27
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
28
+ return this.#handler.onHeaders?.(statusCode, rawHeaders, resume, statusMessage)
29
+ }
30
+
31
+ onUpgrade (statusCode, rawHeaders, socket) {
32
+ return this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
33
+ }
34
+
35
+ onData (data) {
36
+ return this.#handler.onData?.(data)
37
+ }
38
+
39
+ onComplete (trailers) {
40
+ return this.#handler.onComplete?.(trailers)
41
+ }
42
+
43
+ onError (err) {
44
+ if (!this.#handler.onError) {
45
+ throw err
46
+ }
47
+
48
+ return this.#handler.onError?.(err)
49
+ }
50
+
51
+ // Wrap Interface
52
+
53
+ onRequestStart (controller, context) {
54
+ this.#handler.onConnect?.((reason) => controller.abort(reason), context)
55
+ }
56
+
57
+ onRequestUpgrade (controller, statusCode, headers, socket) {
58
+ const rawHeaders = []
59
+ for (const [key, val] of Object.entries(headers)) {
60
+ rawHeaders.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val))
61
+ }
62
+
63
+ this.#handler.onUpgrade?.(statusCode, rawHeaders, socket)
64
+ }
65
+
66
+ onResponseStart (controller, statusCode, headers, statusMessage) {
67
+ const rawHeaders = []
68
+ for (const [key, val] of Object.entries(headers)) {
69
+ rawHeaders.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val))
70
+ }
71
+
72
+ if (this.#handler.onHeaders?.(statusCode, rawHeaders, () => controller.resume(), statusMessage) === false) {
73
+ controller.pause()
74
+ }
75
+ }
76
+
77
+ onResponseData (controller, data) {
78
+ if (this.#handler.onData?.(data) === false) {
79
+ controller.pause()
80
+ }
81
+ }
82
+
83
+ onResponseEnd (controller, trailers) {
84
+ const rawTrailers = []
85
+ for (const [key, val] of Object.entries(trailers)) {
86
+ rawTrailers.push(Buffer.from(key, 'latin1'), toRawHeaderValue(val))
87
+ }
88
+
89
+ this.#handler.onComplete?.(rawTrailers)
90
+ }
91
+
92
+ onResponseError (controller, err) {
93
+ if (!this.#handler.onError) {
94
+ throw new InvalidArgumentError('invalid onError method')
95
+ }
96
+
97
+ this.#handler.onError?.(err)
98
+ }
99
+ }
100
+
101
+ function toRawHeaderValue (value) {
102
+ return Array.isArray(value)
103
+ ? value.map((item) => Buffer.from(item, 'latin1'))
104
+ : Buffer.from(value, 'latin1')
105
+ }