@apifuse/provider-sdk 2.2.0-beta.61 → 2.2.0-beta.63

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 (258) hide show
  1. package/AUTHORING.md +268 -9
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +1 -0
  4. package/bin/apifuse-check.ts +84 -2
  5. package/bin/apifuse-dev.ts +11 -31
  6. package/bin/apifuse-migrate-deployment.ts +200 -0
  7. package/bin/apifuse-pack-check.ts +11 -4
  8. package/bin/apifuse-pack-types.ts +191 -4
  9. package/bin/apifuse-record.ts +44 -32
  10. package/bin/apifuse-test.ts +3 -1
  11. package/dist/cli/commands.d.ts +1 -1
  12. package/dist/cli/commands.js +11 -0
  13. package/dist/cli/deployment-intent.d.ts +178 -0
  14. package/dist/cli/deployment-intent.js +277 -0
  15. package/dist/cli/migrate-deployment-intent.d.ts +132 -0
  16. package/dist/cli/migrate-deployment-intent.js +1552 -0
  17. package/dist/cli/migrate-provider-shape.d.ts +5 -1
  18. package/dist/cli/migrate-provider-shape.js +4 -2
  19. package/dist/cli/resolver-runtime.d.ts +21 -0
  20. package/dist/cli/resolver-runtime.js +31 -0
  21. package/dist/define.d.ts +3 -0
  22. package/dist/define.js +5 -0
  23. package/dist/dev.d.ts +3 -1
  24. package/dist/dev.js +6 -1
  25. package/dist/engine.d.ts +35 -1
  26. package/dist/engine.js +21 -2
  27. package/dist/error-resolution.d.ts +1 -0
  28. package/dist/error-resolution.js +54 -0
  29. package/dist/errors.d.ts +39 -1
  30. package/dist/errors.js +50 -0
  31. package/dist/handle-meta.d.ts +18 -2
  32. package/dist/handle-meta.js +30 -2
  33. package/dist/handle.d.ts +33 -12
  34. package/dist/handle.js +69 -24
  35. package/dist/i18n/error-messages.d.ts +86 -0
  36. package/dist/i18n/error-messages.js +192 -0
  37. package/dist/i18n/index.d.ts +1 -0
  38. package/dist/i18n/index.js +1 -0
  39. package/dist/i18n/keys.d.ts +12 -0
  40. package/dist/i18n/keys.js +76 -5
  41. package/dist/index.d.ts +10 -2
  42. package/dist/index.js +9 -1
  43. package/dist/lint.d.ts +10 -0
  44. package/dist/lint.js +440 -57
  45. package/dist/provider.d.ts +7 -5
  46. package/dist/provider.js +4 -2
  47. package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
  48. package/dist/runtime/browser-telemetry-binding.js +83 -0
  49. package/dist/runtime/browser-telemetry.d.ts +76 -0
  50. package/dist/runtime/browser-telemetry.js +165 -0
  51. package/dist/runtime/browser.d.ts +3 -0
  52. package/dist/runtime/browser.js +208 -47
  53. package/dist/runtime/cache-telemetry.d.ts +71 -0
  54. package/dist/runtime/cache-telemetry.js +177 -0
  55. package/dist/runtime/cache.js +67 -7
  56. package/dist/runtime/capability-response.d.ts +3 -0
  57. package/dist/runtime/capability-response.js +56 -0
  58. package/dist/runtime/capability-telemetry.d.ts +46 -0
  59. package/dist/runtime/capability-telemetry.js +68 -0
  60. package/dist/runtime/diagnostic-env.d.ts +3 -0
  61. package/dist/runtime/diagnostic-env.js +10 -1
  62. package/dist/runtime/engine-mode.d.ts +74 -0
  63. package/dist/runtime/engine-mode.js +152 -0
  64. package/dist/runtime/handle.js +15 -10
  65. package/dist/runtime/http-telemetry-binding.d.ts +10 -0
  66. package/dist/runtime/http-telemetry-binding.js +152 -0
  67. package/dist/runtime/http-telemetry-guard.d.ts +16 -0
  68. package/dist/runtime/http-telemetry-guard.js +202 -0
  69. package/dist/runtime/http-telemetry.d.ts +104 -0
  70. package/dist/runtime/http-telemetry.js +190 -0
  71. package/dist/runtime/http.d.ts +31 -0
  72. package/dist/runtime/http.js +356 -174
  73. package/dist/runtime/instrumentation.js +92 -13
  74. package/dist/runtime/ocr-telemetry.d.ts +82 -0
  75. package/dist/runtime/ocr-telemetry.js +192 -0
  76. package/dist/runtime/ocr.js +30 -8
  77. package/dist/runtime/request-telemetry.d.ts +2 -0
  78. package/dist/runtime/request-telemetry.js +2 -1
  79. package/dist/runtime/resolver-runtime-options.d.ts +14 -0
  80. package/dist/runtime/resolver-runtime-options.js +33 -0
  81. package/dist/runtime/resolver-shared.d.ts +3 -2
  82. package/dist/runtime/resolver-shared.js +4 -4
  83. package/dist/runtime/resolver-telemetry.d.ts +1 -0
  84. package/dist/runtime/resolver-telemetry.js +1 -1
  85. package/dist/runtime/resolver-vendors/browser.d.ts +2 -0
  86. package/dist/runtime/resolver-vendors/browser.js +46 -0
  87. package/dist/runtime/resolver-vendors/hosts.d.ts +21 -0
  88. package/dist/runtime/resolver-vendors/hosts.js +35 -0
  89. package/dist/runtime/resolver-vendors/hypersolutions.js +2 -2
  90. package/dist/runtime/resolver-vendors/types.d.ts +5 -4
  91. package/dist/runtime/resolver.d.ts +4 -1
  92. package/dist/runtime/resolver.js +46 -13
  93. package/dist/runtime/state-telemetry.d.ts +79 -0
  94. package/dist/runtime/state-telemetry.js +194 -0
  95. package/dist/runtime/state.d.ts +3 -0
  96. package/dist/runtime/state.js +26 -7
  97. package/dist/runtime/stealth-cookies.d.ts +5 -1
  98. package/dist/runtime/stealth-cookies.js +21 -1
  99. package/dist/runtime/stealth-owned-headers.d.ts +26 -0
  100. package/dist/runtime/stealth-owned-headers.js +37 -0
  101. package/dist/runtime/stealth-telemetry.d.ts +92 -0
  102. package/dist/runtime/stealth-telemetry.js +179 -0
  103. package/dist/runtime/stealth.d.ts +4 -1
  104. package/dist/runtime/stealth.js +228 -85
  105. package/dist/runtime/stt-telemetry.d.ts +77 -0
  106. package/dist/runtime/stt-telemetry.js +176 -0
  107. package/dist/runtime/stt.js +15 -6
  108. package/dist/server/error-stack-frames.d.ts +35 -0
  109. package/dist/server/error-stack-frames.js +219 -0
  110. package/dist/server/index.d.ts +9 -0
  111. package/dist/server/index.js +5 -0
  112. package/dist/server/sdk-version.d.ts +3 -0
  113. package/dist/server/sdk-version.js +19 -0
  114. package/dist/server/self-test.js +1 -18
  115. package/dist/server/sensitive-values.d.ts +1 -1
  116. package/dist/server/sensitive-values.js +9 -0
  117. package/dist/server/serve-implementation.d.ts +53 -2
  118. package/dist/server/serve-implementation.js +369 -123
  119. package/dist/server/types.d.ts +1 -0
  120. package/dist/server/types.js +7 -0
  121. package/dist/stateful/stateful-provider-owner-forwarder.js +22 -0
  122. package/dist/stateful/stateful-provider-runtime-context.d.ts +3 -0
  123. package/dist/stateful/stateful-provider-runtime-context.js +1 -0
  124. package/dist/types.d.ts +94 -1
  125. package/package.json +6 -1
  126. package/src/cli/commands.ts +14 -0
  127. package/src/cli/deployment-intent.ts +364 -0
  128. package/src/cli/migrate-deployment-intent.ts +1908 -0
  129. package/src/cli/migrate-provider-shape.ts +10 -4
  130. package/src/cli/resolver-runtime.ts +72 -0
  131. package/src/define.ts +13 -0
  132. package/src/dev.ts +9 -2
  133. package/src/engine.ts +38 -2
  134. package/src/error-resolution.ts +58 -0
  135. package/src/errors.ts +77 -1
  136. package/src/handle-meta.ts +39 -4
  137. package/src/handle.ts +121 -38
  138. package/src/i18n/error-messages.ts +255 -0
  139. package/src/i18n/index.ts +1 -0
  140. package/src/i18n/keys.ts +80 -6
  141. package/src/index.ts +91 -0
  142. package/src/lint.ts +552 -57
  143. package/src/provider.ts +20 -0
  144. package/src/runtime/browser-telemetry-binding.ts +92 -0
  145. package/src/runtime/browser-telemetry.ts +289 -0
  146. package/src/runtime/browser.ts +249 -65
  147. package/src/runtime/cache-telemetry.ts +247 -0
  148. package/src/runtime/cache.ts +68 -17
  149. package/src/runtime/capability-response.ts +53 -0
  150. package/src/runtime/capability-telemetry.ts +101 -0
  151. package/src/runtime/diagnostic-env.ts +11 -1
  152. package/src/runtime/engine-mode.ts +233 -0
  153. package/src/runtime/handle.ts +20 -10
  154. package/src/runtime/http-telemetry-binding.ts +162 -0
  155. package/src/runtime/http-telemetry-guard.ts +208 -0
  156. package/src/runtime/http-telemetry.ts +292 -0
  157. package/src/runtime/http.ts +457 -204
  158. package/src/runtime/instrumentation.ts +105 -16
  159. package/src/runtime/ocr-telemetry.ts +308 -0
  160. package/src/runtime/ocr.ts +109 -74
  161. package/src/runtime/request-telemetry.ts +2 -1
  162. package/src/runtime/resolver-runtime-options.ts +50 -0
  163. package/src/runtime/resolver-shared.ts +9 -4
  164. package/src/runtime/resolver-telemetry.ts +1 -1
  165. package/src/runtime/resolver-vendors/browser.ts +65 -7
  166. package/src/runtime/resolver-vendors/hosts.ts +43 -0
  167. package/src/runtime/resolver-vendors/hypersolutions.ts +2 -2
  168. package/src/runtime/resolver-vendors/types.ts +5 -4
  169. package/src/runtime/resolver.ts +64 -21
  170. package/src/runtime/state-telemetry.ts +298 -0
  171. package/src/runtime/state.ts +46 -45
  172. package/src/runtime/stealth-cookies.ts +24 -1
  173. package/src/runtime/stealth-owned-headers.ts +41 -0
  174. package/src/runtime/stealth-telemetry.ts +304 -0
  175. package/src/runtime/stealth.ts +355 -150
  176. package/src/runtime/stt-telemetry.ts +282 -0
  177. package/src/runtime/stt.ts +93 -71
  178. package/src/server/error-stack-frames.ts +214 -0
  179. package/src/server/index.ts +66 -0
  180. package/src/server/sdk-version.ts +22 -0
  181. package/src/server/self-test.ts +1 -21
  182. package/src/server/sensitive-values.ts +14 -0
  183. package/src/server/serve-implementation.ts +601 -157
  184. package/src/server/types.ts +7 -0
  185. package/src/stateful/stateful-provider-owner-forwarder.ts +23 -0
  186. package/src/stateful/stateful-provider-runtime-context.ts +4 -0
  187. package/src/types.ts +99 -1
  188. package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +0 -6
  189. package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +0 -3
  190. package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +0 -10
  191. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-a.ts.txt +0 -3
  192. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-b.ts.txt +0 -3
  193. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-barrel.ts.txt +0 -2
  194. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-ambiguous-index.ts.txt +0 -3
  195. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-computed.ts.txt +0 -6
  196. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-a.ts.txt +0 -1
  197. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-b.ts.txt +0 -1
  198. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-cycle-index.ts.txt +0 -3
  199. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-barrel.ts.txt +0 -7
  200. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-catalog.ts.txt +0 -19
  201. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-docs.ts.txt +0 -6
  202. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-index.ts.txt +0 -3
  203. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-daiso-stores.ts.txt +0 -8
  204. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-indirect-config.ts.txt +0 -5
  205. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-nonstatic.ts.txt +0 -4
  206. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-package-index.ts.txt +0 -3
  207. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-index.ts.txt +0 -3
  208. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-leaf.ts.txt +0 -10
  209. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-one.ts.txt +0 -1
  210. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-reexport-two.ts.txt +0 -1
  211. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-index.ts.txt +0 -3
  212. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-one.ts.txt +0 -6
  213. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-composed-two.ts.txt +0 -6
  214. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-mixed-index.ts.txt +0 -14
  215. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-runtime-registry.ts.txt +0 -3
  216. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-barrel.ts.txt +0 -2
  217. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-index.ts.txt +0 -3
  218. package/src/cli/__tests__/fixtures/migrate-operation-declaration/discovery-star-package-leaf.ts.txt +0 -3
  219. package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +0 -8
  220. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +0 -15
  221. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +0 -9
  222. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-kebab-operation-id.ts.txt +0 -17
  223. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +0 -5
  224. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +0 -24
  225. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +0 -16
  226. package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-snake-operation-id.ts.txt +0 -21
  227. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-duplicate-provider.ts.txt +0 -11
  228. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-id-unresolved.ts.txt +0 -7
  229. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +0 -3
  230. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-many.ts.txt +0 -18
  231. package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-one-to-one.ts.txt +0 -13
  232. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +0 -31
  233. package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +0 -11
  234. package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +0 -11
  235. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +0 -11
  236. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +0 -21
  237. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +0 -11
  238. package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +0 -14
  239. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-en.json +0 -1
  240. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ja.json +0 -1
  241. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-canonical-ko.json +0 -1
  242. package/src/cli/__tests__/fixtures/migrate-operation-declaration/locale-existing-operation-namespace-en.json +0 -8
  243. package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +0 -7
  244. package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +0 -6
  245. package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +0 -7
  246. package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +0 -6
  247. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-binding-ambiguous.ts.txt +0 -12
  248. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported-other.ts.txt +0 -7
  249. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-imported.ts.txt +0 -5
  250. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-key-ambiguous.ts.txt +0 -19
  251. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-shorthand.ts.txt +0 -9
  252. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-typed.ts.txt +0 -11
  253. package/src/cli/__tests__/fixtures/migrate-operation-declaration/registry-unrelated.ts.txt +0 -13
  254. package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +0 -7
  255. package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +0 -7
  256. package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +0 -15
  257. package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +0 -4
  258. package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +0 -12
@@ -2,9 +2,34 @@ import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, re
2
2
  import { HttpRedirectError, ProviderError, TransportError } from "../errors.js";
3
3
  import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
4
4
  import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
5
+ import { httpTelemetryErrorCode, } from "./http-telemetry.js";
5
6
  import { evaluateRedirectHop, isRedirectStatus, resolveRedirectUrl } from "./redirects.js";
6
7
  import { normalizeHttpRequestBody, redactSensitiveError, redactSensitiveRequestError, serializeRequestUrl, } from "./request-options.js";
8
+ import { startHttpTelemetry } from "./http-telemetry-guard.js";
9
+ import { scopedHttpTelemetry } from "./http-telemetry-binding.js";
7
10
  const DEFAULT_HTTP_BASE_URL = "http://localhost";
11
+ function httpAttemptFailure(error, status) {
12
+ try {
13
+ return {
14
+ e: httpTelemetryErrorCode(proxyTransportRetryErrorCode(error)),
15
+ status: proxyTransportRetryErrorStatus(error) ?? status,
16
+ ...(error instanceof Error
17
+ ? {
18
+ diagnostics: {
19
+ name: error.name,
20
+ message: error.message,
21
+ ...(error.cause instanceof Error
22
+ ? { cause: { name: error.cause.name, message: error.cause.message } }
23
+ : {}),
24
+ },
25
+ }
26
+ : {}),
27
+ };
28
+ }
29
+ catch {
30
+ return { e: "other" };
31
+ }
32
+ }
8
33
  function isHttpStatusOutcome(outcome) {
9
34
  return "kind" in outcome && outcome.kind === "http-status";
10
35
  }
@@ -59,19 +84,92 @@ function hasHeader(headers, name) {
59
84
  const needle = name.toLowerCase();
60
85
  return Object.keys(headers).some((key) => key.toLowerCase() === needle);
61
86
  }
62
- function withClientHeaders(options, clientOptions, body) {
63
- const headers = {
87
+ function withClientHeaderDefaults(headers, clientOptions, body) {
88
+ const merged = {
64
89
  ...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
65
- ...options?.headers,
90
+ ...headers,
66
91
  };
67
- if (body !== undefined && !hasHeader(headers, "Content-Type")) {
68
- headers["Content-Type"] = "application/json";
92
+ if (body !== undefined && !hasHeader(merged, "Content-Type")) {
93
+ merged["Content-Type"] = "application/json";
69
94
  }
95
+ return merged;
96
+ }
97
+ function withClientHeaders(options, clientOptions, body) {
98
+ // The ambient client signal is merged into the per-request one here so every
99
+ // downstream read (retry loop, sleep, body consumption) sees a single signal.
100
+ // The header-factory branch needs it too, otherwise factory-header requests
101
+ // would lose ambient cancellation entirely.
102
+ const signal = mergeAbortSignals(clientOptions.signal, options?.signal);
103
+ // A header factory is resolved per issued attempt (resolveAttemptHeaders);
104
+ // the client defaults are applied to its output at that point.
105
+ if (typeof options?.headers === "function")
106
+ return { ...options, signal };
70
107
  return {
71
108
  ...options,
72
- headers,
109
+ headers: withClientHeaderDefaults(options?.headers, clientOptions, body),
110
+ signal,
73
111
  };
74
112
  }
113
+ function headerFactoryFailure(message, cause) {
114
+ // A dedicated code keeps a broken proof minter out of the retry predicate:
115
+ // toHttpTransportError would otherwise brand a plain throw as
116
+ // transport_network_error, which the default retry policy re-issues.
117
+ // retryable:false is explicit so the served error envelope does not
118
+ // advertise a deterministic signing failure as retryable, and
119
+ // category:provider_error keeps a provider-local minting bug out of the
120
+ // upstream-failure signal (status 0 would otherwise fall through the
121
+ // observability ladder to upstream_http / source upstream_failure).
122
+ return new TransportError(message, {
123
+ code: "http_header_factory_failed",
124
+ status: 0,
125
+ retryable: false,
126
+ category: "provider_error",
127
+ ...(cause instanceof Error ? { cause } : {}),
128
+ });
129
+ }
130
+ function isHeaderRecord(value) {
131
+ return value !== null && typeof value === "object" && !Array.isArray(value);
132
+ }
133
+ // Settle `promise`, or reject with the signal's reason as soon as it aborts, so
134
+ // a pending header factory cannot outlive the request deadline or an ambient
135
+ // cancellation (native fetch has not started yet, so nothing else observes them).
136
+ function settleBeforeAbort(promise, signal) {
137
+ if (!signal)
138
+ return promise;
139
+ return new Promise((resolve, reject) => {
140
+ const onAbort = () => reject(signal.reason);
141
+ if (signal.aborted)
142
+ onAbort();
143
+ else
144
+ signal.addEventListener("abort", onAbort, { once: true });
145
+ // Always attach handlers: a late rejection after the abort must settle
146
+ // here (a no-op on the already-rejected promise), never go unhandled.
147
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
148
+ });
149
+ }
150
+ async function resolveAttemptHeaders(options, clientOptions, attempt, signal) {
151
+ const { headers } = options;
152
+ if (typeof headers !== "function")
153
+ return headers;
154
+ // The per-call timeout may have fired during proxy resolution: do not start
155
+ // the factory at all once the request is already aborted.
156
+ if (signal?.aborted)
157
+ throw signal.reason;
158
+ let minted;
159
+ try {
160
+ minted = await settleBeforeAbort(Promise.resolve().then(() => headers(attempt())), signal);
161
+ }
162
+ catch (error) {
163
+ // An abort is classified by toHttpTransportError (cancelled / timed out).
164
+ if (signal?.aborted)
165
+ throw error;
166
+ throw headerFactoryFailure("Request header factory failed", error);
167
+ }
168
+ if (!isHeaderRecord(minted)) {
169
+ throw headerFactoryFailure("Request header factory must return a header record");
170
+ }
171
+ return withClientHeaderDefaults(minted, clientOptions, options.body);
172
+ }
75
173
  function parseHttpData(body, headers) {
76
174
  const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
77
175
  if (contentType?.includes("application/json")) {
@@ -527,22 +625,29 @@ function serializeHttpRequestUrl(baseUrl, url, options) {
527
625
  throw redactSensitiveRequestError(error, url, options.sensitiveParams);
528
626
  }
529
627
  }
530
- async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
628
+ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe, observeProxy, nextHeaderAttempt = () => 1) {
531
629
  const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
532
630
  const { requestUrl } = serializedUrl;
533
631
  const controller = options.timeout ? new AbortController() : undefined;
534
- const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
535
- const timeoutHandle = options.timeout
632
+ const signal = mergeAbortSignals(options.signal, controller?.signal);
633
+ let timeoutHandle = options.timeout
536
634
  ? setTimeout(() => controller?.abort(), options.timeout)
537
635
  : undefined;
636
+ const disarmTimeout = () => {
637
+ if (!timeoutHandle)
638
+ return;
639
+ clearTimeout(timeoutHandle);
640
+ timeoutHandle = undefined;
641
+ };
538
642
  let proxy;
539
643
  try {
540
- throwIfAmbientAborted(clientOptions.signal);
644
+ throwIfAmbientAborted(options.signal);
541
645
  // Resolve inside the try (and after the timeout is armed) so allocator
542
646
  // failures are branded as TransportErrors and count against the request
543
647
  // deadline, exactly as an inline resolve would.
544
648
  proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
545
- throwIfAmbientAborted(clientOptions.signal);
649
+ observeProxy?.(Boolean(proxy));
650
+ throwIfAmbientAborted(options.signal);
546
651
  // For a registry allocator chain, skip an endpoint a prior attempt already
547
652
  // tried rather than re-issuing the same request. Returning the sentinel
548
653
  // (instead of breaking) lets the loop keep advancing the flat offset until
@@ -553,8 +658,12 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
553
658
  }
554
659
  dedupe.attempted.add(proxy);
555
660
  }
661
+ // Resolve after the dedupe check so a skipped duplicate offset never burns
662
+ // a single-use proof, and with the serialized URL so URL-bound proofs
663
+ // match what is actually issued.
664
+ const requestHeaders = await resolveAttemptHeaders(options, clientOptions, () => ({ attempt: nextHeaderAttempt(), url: requestUrl, method }), signal);
556
665
  const requestInit = {
557
- headers: options.headers,
666
+ headers: requestHeaders,
558
667
  method,
559
668
  ...(proxy ? { proxy } : {}),
560
669
  ...(signal ? { signal } : {}),
@@ -563,6 +672,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
563
672
  requestInit.body = normalizeNativeFetchBody(options.body);
564
673
  }
565
674
  const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
675
+ observeProxy?.(Boolean(proxy), response.status);
566
676
  const headers = Object.fromEntries(response.headers.entries());
567
677
  if (statusRetryCodes && response.status >= 400) {
568
678
  await drainNativeResponseBody(response);
@@ -581,35 +691,45 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
581
691
  status: response.status,
582
692
  });
583
693
  }
584
- return toNativeHttpResponse(response);
694
+ // `timeout` bounds the header phase (types.ts RequestOptions.timeout), so it
695
+ // is disarmed before the body is read: awaiting the buffered body inside
696
+ // this try brands a mid-body failure as a transport error, but must not
697
+ // extend the deadline over the download. Cancellation still applies —
698
+ // `signal` stays attached to the native response. The discarded non-2xx
699
+ // bodies drained above stay under the deadline on purpose: nothing else
700
+ // bounds them, and their content is never returned to the caller.
701
+ disarmTimeout();
702
+ return await toNativeHttpResponse(response);
585
703
  }
586
704
  catch (error) {
587
705
  if (error instanceof SyntaxError) {
588
706
  throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
589
707
  }
590
- const transportError = redactSensitiveError(toHttpTransportError(error, clientOptions.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
708
+ const transportError = redactSensitiveError(toHttpTransportError(error, options.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
591
709
  transportError.proxyUsed = Boolean(proxy);
592
710
  throw transportError;
593
711
  }
594
712
  finally {
595
- if (timeoutHandle)
596
- clearTimeout(timeoutHandle);
713
+ disarmTimeout();
597
714
  }
598
715
  }
599
- async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn) {
716
+ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn, observeProxy) {
600
717
  const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
601
718
  const { requestUrl } = serializedUrl;
602
719
  const controller = options.timeout ? new AbortController() : undefined;
603
- const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
720
+ const signal = mergeAbortSignals(options.signal, controller?.signal);
604
721
  const timeoutHandle = options.timeout
605
722
  ? setTimeout(() => controller?.abort(), options.timeout)
606
723
  : undefined;
607
724
  try {
608
- throwIfAmbientAborted(clientOptions.signal);
725
+ throwIfAmbientAborted(options.signal);
609
726
  const proxy = await resolveNativeProxy(options, clientOptions, warn);
610
- throwIfAmbientAborted(clientOptions.signal);
727
+ observeProxy?.(Boolean(proxy));
728
+ throwIfAmbientAborted(options.signal);
729
+ // Streams never retry: a header factory is resolved once, as attempt 1.
730
+ const headers = await resolveAttemptHeaders(options, clientOptions, () => ({ attempt: 1, url: requestUrl, method }), signal);
611
731
  const requestInit = {
612
- headers: options.headers,
732
+ headers,
613
733
  method,
614
734
  ...(proxy ? { proxy } : {}),
615
735
  ...(signal ? { signal } : {}),
@@ -625,15 +745,15 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
625
745
  status: response.status,
626
746
  });
627
747
  }
628
- // Per-call timeout remains header-scoped, while the ambient request signal
629
- // stays attached to the response body for its full consumption lifetime.
630
- return toNativeHttpStreamResponse(response, serializedUrl, clientOptions.signal);
748
+ // The stream timeout remains header-scoped; composed ambient and local
749
+ // cancellation stays attached to native fetch for the body's full lifetime.
750
+ return toNativeHttpStreamResponse(response, serializedUrl, options.signal);
631
751
  }
632
752
  catch (error) {
633
753
  if (error instanceof SyntaxError) {
634
754
  throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
635
755
  }
636
- throw redactSensitiveError(toHttpTransportError(error, clientOptions.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
756
+ throw redactSensitiveError(toHttpTransportError(error, options.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
637
757
  }
638
758
  finally {
639
759
  if (timeoutHandle)
@@ -676,164 +796,198 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
676
796
  throw redactSensitiveRequestError(error, url, options.sensitiveParams);
677
797
  }
678
798
  })();
679
- const retryEnabled = Boolean(retryOptions &&
680
- retryOptions.attempts > 1 &&
681
- isProxyTransportRetryMethod(methodName, retryOptions));
682
- const statusRetryEnabled = Boolean(retryEnabled &&
683
- explicitRetry &&
684
- retryOptions &&
685
- retryOptions.statusCodes.length > 0 &&
686
- headersOptions.throwOnHttpError !== false);
687
- const attemptOptions = statusRetryEnabled
688
- ? { ...headersOptions, throwOnHttpError: false }
689
- : headersOptions;
690
- // Span the whole vendor chain on transport failures. Like ctx.stealth, a
691
- // policy-managed proxy resolves a *different* endpoint/vendor per attempt
692
- // (the flat proxyAttemptOffset rotates across the concatenated vendor pool
693
- // spans), so a transport failure should advance to the next endpoint —
694
- // potentially crossing into the fallback vendor — rather than stopping at
695
- // the per-endpoint retry budget and stranding the request on the primary
696
- // vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
697
- // only for implicit, safe-method allocator requests; explicit retry
698
- // policies (their documented `attempts` ceiling), unsafe methods, and
699
- // static/non-registry vendors keep the retry budget. Status-code retries
700
- // stay bounded by the retry budget regardless; only transport rotation gets
701
- // the full span.
702
- const policyProxy = (() => {
703
- const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
704
- return policy && typeof policy === "object" ? policy : undefined;
705
- })();
706
- const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
707
- const transportAttemptCap = retryOptions
708
- ? resolvePolicyTransportAttemptCap({
799
+ const requestStarted = performance.now();
800
+ const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), retryOptions?.preset);
801
+ try {
802
+ return await runRequest();
803
+ }
804
+ finally {
805
+ telemetry.finish(performance.now() - requestStarted);
806
+ }
807
+ async function runRequest() {
808
+ const retryEnabled = Boolean(retryOptions &&
809
+ retryOptions.attempts > 1 &&
810
+ isProxyTransportRetryMethod(methodName, retryOptions));
811
+ const statusRetryEnabled = Boolean(retryEnabled &&
812
+ explicitRetry &&
813
+ retryOptions &&
814
+ retryOptions.statusCodes.length > 0 &&
815
+ headersOptions.throwOnHttpError !== false);
816
+ const attemptOptions = statusRetryEnabled
817
+ ? { ...headersOptions, throwOnHttpError: false }
818
+ : headersOptions;
819
+ // Span the whole vendor chain on transport failures. Like ctx.stealth, a
820
+ // policy-managed proxy resolves a *different* endpoint/vendor per attempt
821
+ // (the flat proxyAttemptOffset rotates across the concatenated vendor pool
822
+ // spans), so a transport failure should advance to the next endpoint —
823
+ // potentially crossing into the fallback vendor — rather than stopping at
824
+ // the per-endpoint retry budget and stranding the request on the primary
825
+ // vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
826
+ // only for implicit, safe-method allocator requests; explicit retry
827
+ // policies (their documented `attempts` ceiling), unsafe methods, and
828
+ // static/non-registry vendors keep the retry budget. Status-code retries
829
+ // stay bounded by the retry budget regardless; only transport rotation gets
830
+ // the full span.
831
+ const policyProxy = (() => {
832
+ const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
833
+ return policy && typeof policy === "object" ? policy : undefined;
834
+ })();
835
+ const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
836
+ const transportAttemptCap = retryOptions
837
+ ? resolvePolicyTransportAttemptCap({
838
+ policy: policyProxy,
839
+ usesPolicyAllocator,
840
+ retryAttempts: retryOptions.attempts,
841
+ explicitRetry,
842
+ method: methodName,
843
+ })
844
+ : 1;
845
+ // Track resolved endpoints across a policy-allocator chain. Successive
846
+ // attempts rotate the flat offset across the concatenated vendor pool
847
+ // spans, but an under-filled allocation (fewer live endpoints than the
848
+ // configured pool size) makes the modulo mapping repeat endpoints before
849
+ // the offset reaches the next vendor. Rather than re-hammering an
850
+ // already-tried endpoint under backoff, fetchNativeHttp returns a skip
851
+ // sentinel for a duplicate; the loop then advances the flat offset without
852
+ // issuing the request, so it keeps walking toward — and into — the fallback
853
+ // vendor's pool span instead of stalling on the primary vendor.
854
+ // De-duplication is gated on the SAME predicate that widens the attempt cap
855
+ // (implicit, safe-method, registry-chain rotation). It must NOT engage for
856
+ // an explicit retry policy: there the caller's `attempts` count is the
857
+ // contract and each attempt must issue against whatever endpoint it resolves
858
+ // — even a repeat — instead of being silently skipped (which would collapse
859
+ // a `poolSize: 1` + `attempts: 3` request to a single fetch).
860
+ const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
709
861
  policy: policyProxy,
710
862
  usesPolicyAllocator,
711
- retryAttempts: retryOptions.attempts,
712
863
  explicitRetry,
713
864
  method: methodName,
714
- })
715
- : 1;
716
- // Track resolved endpoints across a policy-allocator chain. Successive
717
- // attempts rotate the flat offset across the concatenated vendor pool
718
- // spans, but an under-filled allocation (fewer live endpoints than the
719
- // configured pool size) makes the modulo mapping repeat endpoints before
720
- // the offset reaches the next vendor. Rather than re-hammering an
721
- // already-tried endpoint under backoff, fetchNativeHttp returns a skip
722
- // sentinel for a duplicate; the loop then advances the flat offset without
723
- // issuing the request, so it keeps walking toward — and into — the fallback
724
- // vendor's pool span instead of stalling on the primary vendor.
725
- // De-duplication is gated on the SAME predicate that widens the attempt cap
726
- // (implicit, safe-method, registry-chain rotation). It must NOT engage for
727
- // an explicit retry policy: there the caller's `attempts` count is the
728
- // contract and each attempt must issue against whatever endpoint it resolves
729
- // — even a repeat — instead of being silently skipped (which would collapse
730
- // a `poolSize: 1` + `attempts: 3` request to a single fetch).
731
- const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
732
- policy: policyProxy,
733
- usesPolicyAllocator,
734
- explicitRetry,
735
- method: methodName,
736
- });
737
- const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
738
- const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext);
739
- if (!retryEnabled || !retryOptions) {
740
- throwIfAmbientAborted(clientOptions.signal);
741
- const outcome = await executeOnce();
742
- if (isDedupeSkipOutcome(outcome)) {
743
- // Single-shot path never de-duplicates (dedupeContext is undefined),
744
- // but keep the union total.
745
- throw new TransportError("HTTP request produced no terminal result", {
746
- code: "retry_exhausted",
747
- });
748
- }
749
- if (isHttpStatusOutcome(outcome)) {
750
- throw toUpstreamHttpError(outcome.status);
751
- }
752
- return outcome;
753
- }
754
- let lastError;
755
- let lastErrorCode;
756
- let lastStatus;
757
- // `attempt` walks the flat proxy offset across the full chain span; `issued`
758
- // counts requests that were actually sent (skipped duplicate offsets do not
759
- // increment it). Retry summaries and the status-retry budget must reflect
760
- // issued requests, not the raw offset, so they stay accurate when partial
761
- // allocations skip offsets.
762
- let issued = 0;
763
- for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
764
- throwIfAmbientAborted(clientOptions.signal);
765
- // Whether this offset actually issued a request (vs. a skipped duplicate),
766
- // so the catch counts a thrown *transport* failure once without
767
- // double-counting a status outcome that already incremented before it
768
- // re-threw as an upstream HTTP error.
769
- let issuedThisAttempt = false;
770
- try {
771
- const outcome = await executeOnce(attempt - 1);
865
+ });
866
+ const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
867
+ // Numbers the attempts a header factory sees: only requests that reached
868
+ // header resolution count, so a failed proxy allocation (which increments
869
+ // the telemetry `issued` count below) or a dedupe-skipped offset does not
870
+ // make the first request that is actually built report `attempt: 2`.
871
+ let headerAttempts = 0;
872
+ const nextHeaderAttempt = () => {
873
+ headerAttempts += 1;
874
+ return headerAttempts;
875
+ };
876
+ const executeOnce = async (proxyAttemptOffset = 0) => {
877
+ const started = performance.now();
878
+ let proxyUsed = false;
879
+ let observedStatus;
880
+ const record = (event) => {
881
+ telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
882
+ };
883
+ try {
884
+ const outcome = await fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext, (used, status) => {
885
+ proxyUsed = used;
886
+ observedStatus = status;
887
+ }, nextHeaderAttempt);
888
+ if (!isDedupeSkipOutcome(outcome))
889
+ record({
890
+ status: outcome.status,
891
+ ...(isHttpStatusOutcome(outcome)
892
+ ? { e: "upstream_http_error", statusRetry: true }
893
+ : {}),
894
+ });
895
+ return outcome;
896
+ }
897
+ catch (error) {
898
+ // Record before the retry loop can catch/rethrow a terminal status.
899
+ record(httpAttemptFailure(error, observedStatus));
900
+ throw error;
901
+ }
902
+ };
903
+ if (!retryEnabled || !retryOptions) {
904
+ throwIfAmbientAborted(headersOptions.signal);
905
+ const outcome = await executeOnce();
772
906
  if (isDedupeSkipOutcome(outcome)) {
773
- // Duplicate endpoint from a partial allocation: advance the flat
774
- // offset without issuing the request (no backoff, not a failure) so
775
- // the loop keeps rotating toward the fallback vendor.
776
- continue;
907
+ // Single-shot path never de-duplicates (dedupeContext is undefined),
908
+ // but keep the union total.
909
+ throw new TransportError("HTTP request produced no terminal result", {
910
+ code: "retry_exhausted",
911
+ });
777
912
  }
778
- issued += 1;
779
- issuedThisAttempt = true;
780
913
  if (isHttpStatusOutcome(outcome)) {
781
- lastStatus = outcome.status;
782
- if (outcome.retryable && issued < retryOptions.attempts) {
783
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), clientOptions.signal);
784
- continue;
785
- }
786
914
  throw toUpstreamHttpError(outcome.status);
787
915
  }
788
- const response = outcome;
789
- if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
790
- throw toUpstreamHttpError(response.status);
791
- }
792
- if (issued > 1) {
793
- const summary = {
794
- attempts: issued,
795
- retries: issued - 1,
796
- ...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
797
- transport: "native",
798
- ...(lastErrorCode ? { lastErrorCode } : {}),
799
- ...(lastStatus ? { lastStatus } : {}),
800
- };
801
- clientOptions.onRetrySummary?.(summary);
802
- }
803
- return response;
916
+ return outcome;
804
917
  }
805
- catch (error) {
806
- throwIfAmbientAborted(clientOptions.signal);
807
- if (!issuedThisAttempt)
918
+ let lastError;
919
+ // `attempt` walks the flat proxy offset across the full chain span; `issued`
920
+ // counts requests that were actually sent (skipped duplicate offsets do not
921
+ // increment it). Retry summaries and the status-retry budget must reflect
922
+ // issued requests, not the raw offset, so they stay accurate when partial
923
+ // allocations skip offsets.
924
+ let issued = 0;
925
+ for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
926
+ throwIfAmbientAborted(headersOptions.signal);
927
+ // Whether this offset actually issued a request (vs. a skipped duplicate),
928
+ // so the catch counts a thrown *transport* failure once without
929
+ // double-counting a status outcome that already incremented before it
930
+ // re-threw as an upstream HTTP error.
931
+ let issuedThisAttempt = false;
932
+ try {
933
+ const outcome = await executeOnce(attempt - 1);
934
+ if (isDedupeSkipOutcome(outcome)) {
935
+ // Duplicate endpoint from a partial allocation: advance the flat
936
+ // offset without issuing the request (no backoff, not a failure) so
937
+ // the loop keeps rotating toward the fallback vendor.
938
+ continue;
939
+ }
808
940
  issued += 1;
809
- lastError = error;
810
- lastErrorCode = proxyTransportRetryErrorCode(error);
811
- lastStatus = proxyTransportRetryErrorStatus(error);
812
- const proxyUsed = Boolean(error.proxyUsed);
813
- if (attempt < transportAttemptCap &&
814
- shouldRetryProxyTransportAttempt({
815
- error,
816
- explicitRetry,
817
- method: methodName,
818
- options: retryOptions,
819
- proxyUsed,
820
- })) {
821
- await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), clientOptions.signal);
822
- continue;
941
+ issuedThisAttempt = true;
942
+ if (isHttpStatusOutcome(outcome)) {
943
+ if (outcome.retryable && issued < retryOptions.attempts) {
944
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), headersOptions.signal);
945
+ continue;
946
+ }
947
+ throw toUpstreamHttpError(outcome.status);
948
+ }
949
+ const response = outcome;
950
+ if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
951
+ throw toUpstreamHttpError(response.status);
952
+ }
953
+ if (issued > 1) {
954
+ const summary = telemetry.toTenantRetryPayload();
955
+ if (summary)
956
+ telemetry.observe(() => clientOptions.onRetrySummary?.(summary));
957
+ }
958
+ return response;
959
+ }
960
+ catch (error) {
961
+ throwIfAmbientAborted(headersOptions.signal);
962
+ if (!issuedThisAttempt)
963
+ issued += 1;
964
+ lastError = error;
965
+ const proxyUsed = Boolean(error.proxyUsed);
966
+ if (attempt < transportAttemptCap &&
967
+ shouldRetryProxyTransportAttempt({
968
+ error,
969
+ explicitRetry,
970
+ method: methodName,
971
+ options: retryOptions,
972
+ proxyUsed,
973
+ })) {
974
+ await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), headersOptions.signal);
975
+ continue;
976
+ }
977
+ throw error;
823
978
  }
824
- throw error;
825
979
  }
980
+ // Reached when the attempt cap is consumed without a terminal outcome —
981
+ // e.g. the final offsets of a partial allocation all resolved to
982
+ // already-tried endpoints and were skipped. Surface the last real transport
983
+ // failure rather than a synthetic exhaustion error.
984
+ if (lastError !== undefined) {
985
+ throw lastError;
986
+ }
987
+ throw new TransportError("HTTP retry exhausted without a terminal result", {
988
+ code: "retry_exhausted",
989
+ });
826
990
  }
827
- // Reached when the attempt cap is consumed without a terminal outcome —
828
- // e.g. the final offsets of a partial allocation all resolved to
829
- // already-tried endpoints and were skipped. Surface the last real transport
830
- // failure rather than a synthetic exhaustion error.
831
- if (lastError !== undefined) {
832
- throw lastError;
833
- }
834
- throw new TransportError("HTTP retry exhausted without a terminal result", {
835
- code: "retry_exhausted",
836
- });
837
991
  }
838
992
  async function streamRequest(url, method, options = {}) {
839
993
  const { headersOptions, methodName } = (() => {
@@ -855,7 +1009,30 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
855
1009
  throw redactSensitiveRequestError(error, url, options.sensitiveParams);
856
1010
  }
857
1011
  })();
858
- return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
1012
+ // Streams never retry. Observe the header fetch without consuming its body.
1013
+ const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), "off");
1014
+ const started = performance.now();
1015
+ const alreadyAborted = headersOptions.signal?.aborted === true;
1016
+ let proxyUsed = false;
1017
+ const record = (event) => {
1018
+ if (alreadyAborted)
1019
+ return;
1020
+ telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
1021
+ };
1022
+ try {
1023
+ const response = await fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce, (used) => {
1024
+ proxyUsed = used;
1025
+ });
1026
+ record({ status: response.status });
1027
+ return response;
1028
+ }
1029
+ catch (error) {
1030
+ record(httpAttemptFailure(error));
1031
+ throw error;
1032
+ }
1033
+ finally {
1034
+ telemetry.finish(performance.now() - started);
1035
+ }
859
1036
  }
860
1037
  return {
861
1038
  request: async (url, options = {}) => request(url, options.method ?? "GET", options),
@@ -865,10 +1042,15 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
865
1042
  delete: async (url, options) => request(url, "DELETE", options),
866
1043
  stream: async (url, options = {}) => streamRequest(url, options.method ?? "GET", options),
867
1044
  sse: async (url, options = {}) => {
868
- const headers = {
869
- Accept: "text/event-stream",
870
- ...options.headers,
871
- };
1045
+ const callerHeaders = options.headers;
1046
+ const headers = typeof callerHeaders === "function"
1047
+ ? async (attempt) => {
1048
+ // Pass a malformed result through unchanged so the factory
1049
+ // validation rejects it instead of spreading it into `{}`.
1050
+ const minted = await callerHeaders(attempt);
1051
+ return isHeaderRecord(minted) ? { Accept: "text/event-stream", ...minted } : minted;
1052
+ }
1053
+ : { Accept: "text/event-stream", ...callerHeaders };
872
1054
  const response = await streamRequest(url, options.method ?? "GET", {
873
1055
  ...options,
874
1056
  headers,