@apifuse/provider-sdk 2.2.0-beta.7 → 2.2.0-beta.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AUTHORING.md +943 -51
- package/CHANGELOG.md +273 -1
- package/LICENSE +21 -0
- package/README.md +117 -29
- package/SUBMISSION.md +3 -2
- package/bin/apifuse-check.ts +293 -13
- package/bin/apifuse-dev.ts +42 -19
- package/bin/apifuse-migrate-deployment.ts +200 -0
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +19 -6
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +736 -38
- package/bin/apifuse-perf.ts +59 -23
- package/bin/apifuse-record.ts +923 -94
- package/bin/apifuse-submit-check.ts +2313 -281
- package/bin/apifuse-test.ts +3 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth-turn/index.js +1 -1
- package/dist/auth.d.ts +14 -0
- package/dist/auth.js +67 -0
- package/dist/ceremonies/index.d.ts +16 -0
- package/dist/ceremonies/index.js +141 -36
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +30 -0
- package/dist/cli/create.d.ts +1 -1
- package/dist/cli/create.js +8 -2
- package/dist/cli/deployment-intent.d.ts +178 -0
- package/dist/cli/deployment-intent.js +277 -0
- package/dist/cli/local-connection.d.ts +111 -0
- package/dist/cli/local-connection.js +151 -0
- package/dist/cli/migrate-deployment-intent.d.ts +132 -0
- package/dist/cli/migrate-deployment-intent.js +1552 -0
- package/dist/cli/migrate-operation-declaration.d.ts +70 -0
- package/dist/cli/migrate-operation-declaration.js +2206 -0
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.d.ts +56 -0
- package/dist/cli/migrate-provider-shape.js +580 -0
- package/dist/cli/resolver-runtime.d.ts +21 -0
- package/dist/cli/resolver-runtime.js +31 -0
- package/dist/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +41 -19
- package/dist/cli/templates/provider/index.ts.tpl +6 -3
- package/dist/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/config/loader.d.ts +202 -16
- package/dist/config/loader.js +491 -141
- package/dist/contract-serialization.js +4 -8
- package/dist/contract-types.d.ts +12 -5
- package/dist/contract.js +24 -10
- package/dist/declaration-validation.d.ts +34 -0
- package/dist/declaration-validation.js +305 -0
- package/dist/define.d.ts +71 -29
- package/dist/define.js +807 -69
- package/dist/dev.d.ts +5 -0
- package/dist/dev.js +8 -1
- package/dist/engine.d.ts +129 -0
- package/dist/engine.js +209 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +18 -0
- package/dist/error-resolution.js +243 -0
- package/dist/errors.d.ts +74 -0
- package/dist/errors.js +99 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +371 -0
- package/dist/handle-meta.d.ts +90 -0
- package/dist/handle-meta.js +73 -0
- package/dist/handle.d.ts +226 -0
- package/dist/handle.js +345 -0
- package/dist/health-scenario.d.ts +1901 -0
- package/dist/health-scenario.js +653 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/i18n/error-messages.d.ts +86 -0
- package/dist/i18n/error-messages.js +192 -0
- package/dist/i18n/index.d.ts +1 -0
- package/dist/i18n/index.js +1 -0
- package/dist/i18n/keys.d.ts +12 -0
- package/dist/i18n/keys.js +76 -5
- package/dist/i18n/operation-locale-namespace.d.ts +2 -0
- package/dist/i18n/operation-locale-namespace.js +7 -0
- package/dist/index.d.ts +40 -15
- package/dist/index.js +29 -11
- package/dist/lint.d.ts +43 -11
- package/dist/lint.js +2512 -177
- package/dist/native-address.d.ts +43 -0
- package/dist/native-address.js +281 -0
- package/dist/native-egress-policy.d.ts +31 -0
- package/dist/native-egress-policy.js +288 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/provider.d.ts +18 -8
- package/dist/provider.js +11 -6
- package/dist/runtime/auth-flow.d.ts +6 -1
- package/dist/runtime/auth-flow.js +8 -0
- package/dist/runtime/bounded-expiring-map.d.ts +21 -0
- package/dist/runtime/bounded-expiring-map.js +64 -0
- package/dist/runtime/browser-telemetry-binding.d.ts +5 -0
- package/dist/runtime/browser-telemetry-binding.js +83 -0
- package/dist/runtime/browser-telemetry.d.ts +76 -0
- package/dist/runtime/browser-telemetry.js +165 -0
- package/dist/runtime/browser.d.ts +4 -0
- package/dist/runtime/browser.js +688 -83
- package/dist/runtime/cache-telemetry.d.ts +71 -0
- package/dist/runtime/cache-telemetry.js +177 -0
- package/dist/runtime/cache.d.ts +3 -0
- package/dist/runtime/cache.js +245 -23
- package/dist/runtime/capability-response.d.ts +3 -0
- package/dist/runtime/capability-response.js +56 -0
- package/dist/runtime/capability-telemetry.d.ts +46 -0
- package/dist/runtime/capability-telemetry.js +68 -0
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/diagnostic-env-values.d.ts +5 -0
- package/dist/runtime/diagnostic-env-values.js +57 -0
- package/dist/runtime/diagnostic-env.d.ts +14 -0
- package/dist/runtime/diagnostic-env.js +44 -0
- package/dist/runtime/diagnostic-redactor.d.ts +54 -0
- package/dist/runtime/diagnostic-redactor.js +571 -0
- package/dist/runtime/egress-lease.d.ts +65 -0
- package/dist/runtime/egress-lease.js +236 -0
- package/dist/runtime/engine-mode.d.ts +74 -0
- package/dist/runtime/engine-mode.js +152 -0
- package/dist/runtime/env.js +14 -1
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +23 -26
- package/dist/runtime/handle-wordlist.d.ts +43 -0
- package/dist/runtime/handle-wordlist.js +256 -0
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +908 -0
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +16 -0
- package/dist/runtime/http-telemetry-guard.js +202 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +34 -0
- package/dist/runtime/http.js +680 -121
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +462 -19
- package/dist/runtime/native-network-errors.d.ts +33 -0
- package/dist/runtime/native-network-errors.js +69 -0
- package/dist/runtime/native-network.d.ts +105 -0
- package/dist/runtime/native-network.js +1433 -0
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -0
- package/dist/runtime/ocr-telemetry.d.ts +82 -0
- package/dist/runtime/ocr-telemetry.js +192 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +468 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +398 -16
- package/dist/runtime/provider.d.ts +7 -0
- package/dist/runtime/provider.js +14 -0
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +147 -0
- package/dist/runtime/proxy-retry-policy.d.ts +4 -1
- package/dist/runtime/proxy-retry-policy.js +11 -2
- package/dist/runtime/proxy-telemetry.d.ts +133 -3
- package/dist/runtime/proxy-telemetry.js +218 -70
- package/dist/runtime/redirect-hop-headers.d.ts +19 -0
- package/dist/runtime/redirect-hop-headers.js +58 -0
- package/dist/runtime/redirects.d.ts +61 -0
- package/dist/runtime/redirects.js +149 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +75 -1
- package/dist/runtime/request-options.js +588 -0
- package/dist/runtime/request-telemetry.d.ts +70 -0
- package/dist/runtime/request-telemetry.js +229 -0
- package/dist/runtime/resolver-config.d.ts +13 -0
- package/dist/runtime/resolver-config.js +14 -0
- package/dist/runtime/resolver-public.d.ts +3 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-runtime-options.d.ts +14 -0
- package/dist/runtime/resolver-runtime-options.js +33 -0
- package/dist/runtime/resolver-shared.d.ts +24 -0
- package/dist/runtime/resolver-shared.js +69 -0
- package/dist/runtime/resolver-span-attributes.d.ts +34 -0
- package/dist/runtime/resolver-span-attributes.js +93 -0
- package/dist/runtime/resolver-telemetry.d.ts +135 -0
- package/dist/runtime/resolver-telemetry.js +204 -0
- package/dist/runtime/resolver-usage.d.ts +32 -0
- package/dist/runtime/resolver-usage.js +54 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +53 -0
- package/dist/runtime/resolver-vendors/bindings.js +45 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +24 -0
- package/dist/runtime/resolver-vendors/browser.js +423 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +544 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +23 -0
- package/dist/runtime/resolver-vendors/hosts.js +68 -0
- package/dist/runtime/resolver-vendors/hypersolutions.d.ts +27 -0
- package/dist/runtime/resolver-vendors/hypersolutions.js +369 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +425 -0
- package/dist/runtime/resolver-vendors/types.d.ts +125 -0
- package/dist/runtime/resolver-vendors/types.js +106 -0
- package/dist/runtime/resolver.d.ts +79 -0
- package/dist/runtime/resolver.js +1094 -0
- package/dist/runtime/state-telemetry.d.ts +79 -0
- package/dist/runtime/state-telemetry.js +194 -0
- package/dist/runtime/state.d.ts +10 -1
- package/dist/runtime/state.js +322 -82
- package/dist/runtime/stealth-akamai-sbsd.d.ts +54 -0
- package/dist/runtime/stealth-akamai-sbsd.js +142 -0
- package/dist/runtime/stealth-cookies.d.ts +24 -0
- package/dist/runtime/stealth-cookies.js +131 -0
- package/dist/runtime/stealth-owned-headers.d.ts +39 -0
- package/dist/runtime/stealth-owned-headers.js +57 -0
- package/dist/runtime/stealth-telemetry.d.ts +92 -0
- package/dist/runtime/stealth-telemetry.js +179 -0
- package/dist/runtime/stealth.d.ts +45 -6
- package/dist/runtime/stealth.js +1775 -320
- package/dist/runtime/stt-telemetry.d.ts +77 -0
- package/dist/runtime/stt-telemetry.js +176 -0
- package/dist/runtime/stt.js +18 -19
- package/dist/runtime/timeout.d.ts +5 -0
- package/dist/runtime/timeout.js +12 -0
- package/dist/runtime/trace-config.d.ts +12 -0
- package/dist/runtime/trace-config.js +62 -0
- package/dist/runtime/trace.d.ts +17 -0
- package/dist/runtime/trace.js +176 -25
- package/dist/runtime-boundary-lint.d.ts +137 -0
- package/dist/runtime-boundary-lint.js +590 -0
- package/dist/schema.d.ts +20 -0
- package/dist/schema.js +72 -2
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/error-stack-frames.d.ts +35 -0
- package/dist/server/error-stack-frames.js +219 -0
- package/dist/server/index.d.ts +21 -2
- package/dist/server/index.js +11 -2
- package/dist/server/runtime-diagnostics.d.ts +165 -0
- package/dist/server/runtime-diagnostics.js +360 -0
- package/dist/server/sdk-version.d.ts +3 -0
- package/dist/server/sdk-version.js +19 -0
- package/dist/server/self-test-input-tokens.d.ts +2 -1
- package/dist/server/self-test-input-tokens.js +18 -14
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/self-test.d.ts +18 -3
- package/dist/server/self-test.js +139 -76
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +137 -0
- package/dist/server/serve-implementation.d.ts +285 -0
- package/dist/server/serve-implementation.js +3172 -0
- package/dist/server/serve.d.ts +1 -75
- package/dist/server/serve.js +1 -1170
- package/dist/server/state-backend-report.d.ts +70 -0
- package/dist/server/state-backend-report.js +113 -0
- package/dist/server/trace-output.d.ts +7 -0
- package/dist/server/trace-output.js +111 -0
- package/dist/server/types.d.ts +37 -5
- package/dist/server/types.js +34 -1
- package/dist/stateful/errors.d.ts +19 -0
- package/dist/stateful/errors.js +24 -0
- package/dist/stateful/http-provider-event-emitter.d.ts +40 -0
- package/dist/stateful/http-provider-event-emitter.js +237 -0
- package/dist/stateful/http-session-owner-registry.d.ts +44 -0
- package/dist/stateful/http-session-owner-registry.js +210 -0
- package/dist/stateful/index.d.ts +18 -0
- package/dist/stateful/index.js +18 -0
- package/dist/stateful/provider-event-delivery-failures.d.ts +32 -0
- package/dist/stateful/provider-event-delivery-failures.js +43 -0
- package/dist/stateful/provider-event-pipeline-metrics.d.ts +46 -0
- package/dist/stateful/provider-event-pipeline-metrics.js +48 -0
- package/dist/stateful/provider-event-pipeline.d.ts +50 -0
- package/dist/stateful/provider-event-pipeline.js +1 -0
- package/dist/stateful/provider-events.d.ts +101 -0
- package/dist/stateful/provider-events.js +289 -0
- package/dist/stateful/session-key.d.ts +15 -0
- package/dist/stateful/session-key.js +86 -0
- package/dist/stateful/stateful-provider-adapter-context.d.ts +5 -0
- package/dist/stateful/stateful-provider-adapter-context.js +42 -0
- package/dist/stateful/stateful-provider-adapter-metrics.d.ts +15 -0
- package/dist/stateful/stateful-provider-adapter-metrics.js +21 -0
- package/dist/stateful/stateful-provider-adapter.d.ts +98 -0
- package/dist/stateful/stateful-provider-adapter.js +287 -0
- package/dist/stateful/stateful-provider-observability.d.ts +62 -0
- package/dist/stateful/stateful-provider-observability.js +161 -0
- package/dist/stateful/stateful-provider-owner-forwarder.d.ts +41 -0
- package/dist/stateful/stateful-provider-owner-forwarder.js +237 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +35 -0
- package/dist/stateful/stateful-provider-runtime-context.js +61 -0
- package/dist/stateful/stateful-provider-runtime-executor.d.ts +34 -0
- package/dist/stateful/stateful-provider-runtime-executor.js +52 -0
- package/dist/stateful/stateful-provider-session-routing.d.ts +67 -0
- package/dist/stateful/stateful-provider-session-routing.js +345 -0
- package/dist/stateful/stateful-provider-session-runtime.d.ts +98 -0
- package/dist/stateful/stateful-provider-session-runtime.js +245 -0
- package/dist/stateful-signing.d.ts +18 -0
- package/dist/stateful-signing.js +27 -0
- package/dist/stealth/profiles.d.ts +15 -4
- package/dist/stealth/profiles.js +169 -211
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.js +7 -1
- package/dist/testing/index.d.ts +2 -1
- package/dist/testing/index.js +2 -1
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +509 -25
- package/dist/trace-sanitization.d.ts +6 -0
- package/dist/trace-sanitization.js +68 -0
- package/dist/types.d.ts +899 -210
- package/dist/types.js +1 -0
- package/dist/typescript-module.d.ts +39 -0
- package/dist/typescript-module.js +72 -0
- package/package.json +52 -6
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/commands.ts +37 -0
- package/src/cli/create.ts +8 -2
- package/src/cli/deployment-intent.ts +364 -0
- package/src/cli/local-connection.ts +221 -0
- package/src/cli/migrate-deployment-intent.ts +1908 -0
- package/src/cli/migrate-operation-declaration.ts +3026 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +778 -0
- package/src/cli/resolver-runtime.ts +72 -0
- package/src/cli/templates/provider/.agents/skills/health-checks-and-fail-closed/SKILL.md.tpl +141 -23
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +41 -19
- package/src/cli/templates/provider/index.ts.tpl +6 -3
- package/src/cli/templates/provider/operations/ping.ts.tpl +4 -1
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/config/loader.ts +774 -181
- package/src/contract-serialization.ts +5 -7
- package/src/contract-types.ts +12 -5
- package/src/contract.ts +24 -10
- package/src/declaration-validation.ts +376 -0
- package/src/define.ts +1105 -131
- package/src/dev.ts +13 -1
- package/src/engine.ts +390 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +269 -0
- package/src/errors.ts +157 -0
- package/src/fixture-sanitization.ts +422 -0
- package/src/handle-meta.ts +143 -0
- package/src/handle.ts +694 -0
- package/src/health-scenario.ts +960 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/i18n/error-messages.ts +255 -0
- package/src/i18n/index.ts +1 -0
- package/src/i18n/keys.ts +80 -6
- package/src/i18n/operation-locale-namespace.ts +10 -0
- package/src/index.ts +390 -23
- package/src/lint.ts +2975 -234
- package/src/native-address.ts +340 -0
- package/src/native-egress-policy.ts +358 -0
- package/src/observability.ts +51 -1
- package/src/provider.ts +207 -27
- package/src/runtime/auth-flow.ts +15 -0
- package/src/runtime/bounded-expiring-map.ts +67 -0
- package/src/runtime/browser-telemetry-binding.ts +92 -0
- package/src/runtime/browser-telemetry.ts +289 -0
- package/src/runtime/browser.ts +882 -99
- package/src/runtime/cache-telemetry.ts +247 -0
- package/src/runtime/cache.ts +258 -22
- package/src/runtime/capability-response.ts +53 -0
- package/src/runtime/capability-telemetry.ts +101 -0
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/diagnostic-env-values.ts +56 -0
- package/src/runtime/diagnostic-env.ts +65 -0
- package/src/runtime/diagnostic-redactor.ts +668 -0
- package/src/runtime/egress-lease.ts +341 -0
- package/src/runtime/engine-mode.ts +233 -0
- package/src/runtime/env.ts +15 -1
- package/src/runtime/executor.ts +39 -32
- package/src/runtime/handle-wordlist.ts +266 -0
- package/src/runtime/handle.ts +1180 -0
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +208 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +896 -150
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +628 -28
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1853 -0
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/ocr-telemetry.ts +308 -0
- package/src/runtime/ocr.ts +564 -0
- package/src/runtime/otlp.ts +468 -21
- package/src/runtime/provider.ts +15 -0
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +222 -0
- package/src/runtime/proxy-retry-policy.ts +12 -1
- package/src/runtime/proxy-telemetry.ts +358 -103
- package/src/runtime/redirect-hop-headers.ts +71 -0
- package/src/runtime/redirects.ts +215 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +728 -1
- package/src/runtime/request-telemetry.ts +375 -0
- package/src/runtime/resolver-config.ts +18 -0
- package/src/runtime/resolver-public.ts +44 -0
- package/src/runtime/resolver-runtime-options.ts +50 -0
- package/src/runtime/resolver-shared.ts +113 -0
- package/src/runtime/resolver-span-attributes.ts +139 -0
- package/src/runtime/resolver-telemetry.ts +362 -0
- package/src/runtime/resolver-usage.ts +99 -0
- package/src/runtime/resolver-vendors/bindings.ts +61 -0
- package/src/runtime/resolver-vendors/browser.ts +591 -0
- package/src/runtime/resolver-vendors/capsolver.ts +727 -0
- package/src/runtime/resolver-vendors/hosts.ts +81 -0
- package/src/runtime/resolver-vendors/hypersolutions.ts +480 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +555 -0
- package/src/runtime/resolver-vendors/types.ts +261 -0
- package/src/runtime/resolver.ts +1543 -0
- package/src/runtime/state-telemetry.ts +298 -0
- package/src/runtime/state.ts +435 -89
- package/src/runtime/stealth-akamai-sbsd.ts +207 -0
- package/src/runtime/stealth-cookies.ts +155 -0
- package/src/runtime/stealth-owned-headers.ts +67 -0
- package/src/runtime/stealth-telemetry.ts +309 -0
- package/src/runtime/stealth.ts +2275 -372
- package/src/runtime/stt-telemetry.ts +282 -0
- package/src/runtime/stt.ts +96 -91
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +257 -35
- package/src/runtime-boundary-lint.ts +707 -0
- package/src/schema.ts +80 -2
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/error-stack-frames.ts +214 -0
- package/src/server/index.ts +184 -2
- package/src/server/runtime-diagnostics.ts +522 -0
- package/src/server/sdk-version.ts +22 -0
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/self-test.ts +205 -89
- package/src/server/sensitive-values.ts +211 -0
- package/src/server/serve-implementation.ts +4596 -0
- package/src/server/serve.ts +1 -1685
- package/src/server/state-backend-report.ts +185 -0
- package/src/server/trace-output.ts +189 -0
- package/src/server/types.ts +34 -1
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +35 -0
- package/src/stateful/http-provider-event-emitter.ts +314 -0
- package/src/stateful/http-session-owner-registry.ts +306 -0
- package/src/stateful/index.ts +18 -0
- package/src/stateful/provider-event-delivery-failures.ts +80 -0
- package/src/stateful/provider-event-pipeline-metrics.ts +95 -0
- package/src/stateful/provider-event-pipeline.ts +61 -0
- package/src/stateful/provider-events.ts +462 -0
- package/src/stateful/session-key.ts +111 -0
- package/src/stateful/stateful-provider-adapter-context.ts +59 -0
- package/src/stateful/stateful-provider-adapter-metrics.ts +48 -0
- package/src/stateful/stateful-provider-adapter.ts +562 -0
- package/src/stateful/stateful-provider-observability.ts +261 -0
- package/src/stateful/stateful-provider-owner-forwarder.ts +310 -0
- package/src/stateful/stateful-provider-runtime-context.ts +96 -0
- package/src/stateful/stateful-provider-runtime-executor.ts +96 -0
- package/src/stateful/stateful-provider-session-routing.ts +546 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +212 -231
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -1
- package/src/testing/index.ts +10 -1
- package/src/testing/run.ts +685 -19
- package/src/trace-sanitization.ts +110 -0
- package/src/types.ts +1014 -273
- package/src/typescript-module.ts +100 -0
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice.d.ts +0 -15
- package/dist/runtime/choice.js +0 -413
- package/src/choice-token.ts +0 -165
- package/src/runtime/choice.ts +0 -686
package/src/runtime/http.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import type { ProxyResolutionOptions } from "../config/loader.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
policyRotatesTransportVendorChain,
|
|
4
|
+
resolvePolicyTransportAttemptCap,
|
|
5
|
+
resolveProxyConfigAsync,
|
|
6
|
+
} from "../config/loader.js";
|
|
3
7
|
import { ProviderError, TransportError } from "../errors.js";
|
|
4
8
|
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
5
9
|
import type {
|
|
10
|
+
HttpAttemptContext,
|
|
6
11
|
HttpClient,
|
|
12
|
+
HttpHeadersFactory,
|
|
7
13
|
HttpMethod,
|
|
14
|
+
HttpRedirectPolicy,
|
|
8
15
|
HttpResponse,
|
|
9
16
|
HttpRetrySummary,
|
|
10
17
|
HttpStreamResponse,
|
|
18
|
+
ProviderProxyPolicy,
|
|
11
19
|
RequestOptions,
|
|
12
20
|
RequestWithMethodOptions,
|
|
13
21
|
SseMessage,
|
|
@@ -23,14 +31,91 @@ import {
|
|
|
23
31
|
shouldRetryProxyTransportAttempt,
|
|
24
32
|
validateUnsafeProxyTransportRetryMethods,
|
|
25
33
|
} from "./proxy-retry-policy.js";
|
|
26
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
httpTelemetryErrorCode,
|
|
36
|
+
type HttpTelemetrySink,
|
|
37
|
+
type HttpAttemptTelemetryEvent,
|
|
38
|
+
} from "./http-telemetry.js";
|
|
39
|
+
import {
|
|
40
|
+
invalidRedirectPolicy,
|
|
41
|
+
isRedirectStatus,
|
|
42
|
+
nextSameOriginRedirectHop,
|
|
43
|
+
normalizeRedirectPolicy,
|
|
44
|
+
type RedirectPolicyTransport,
|
|
45
|
+
} from "./redirects.js";
|
|
46
|
+
import {
|
|
47
|
+
normalizeHttpRequestBody,
|
|
48
|
+
redactSensitiveError,
|
|
49
|
+
redactSensitiveRequestError,
|
|
50
|
+
type SerializedRequestUrl,
|
|
51
|
+
serializeRequestUrl,
|
|
52
|
+
} from "./request-options.js";
|
|
53
|
+
|
|
54
|
+
import { startHttpTelemetry } from "./http-telemetry-guard.js";
|
|
55
|
+
import { scopedHttpTelemetry } from "./http-telemetry-binding.js";
|
|
27
56
|
|
|
28
57
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
29
58
|
|
|
59
|
+
function httpAttemptFailure(
|
|
60
|
+
error: unknown,
|
|
61
|
+
status?: number,
|
|
62
|
+
): Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed"> {
|
|
63
|
+
try {
|
|
64
|
+
return {
|
|
65
|
+
e: httpTelemetryErrorCode(proxyTransportRetryErrorCode(error)),
|
|
66
|
+
status: proxyTransportRetryErrorStatus(error) ?? status,
|
|
67
|
+
...(error instanceof Error
|
|
68
|
+
? {
|
|
69
|
+
diagnostics: {
|
|
70
|
+
name: error.name,
|
|
71
|
+
message: error.message,
|
|
72
|
+
...(error.cause instanceof Error
|
|
73
|
+
? { cause: { name: error.cause.name, message: error.cause.message } }
|
|
74
|
+
: {}),
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
: {}),
|
|
78
|
+
};
|
|
79
|
+
} catch {
|
|
80
|
+
return { e: "other" };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
30
84
|
export type HttpClientOptions = ProxyResolutionOptions & {
|
|
31
85
|
warn?: (message: string) => void;
|
|
32
86
|
userAgent?: string;
|
|
33
87
|
onRetrySummary?: (summary: HttpRetrySummary) => void;
|
|
88
|
+
/**
|
|
89
|
+
* Synchronous observer for HTTP attempts, retries, timeouts, status, duration,
|
|
90
|
+
* and observed proxy use. Observer failures never change transport behavior.
|
|
91
|
+
* All hooks and their property reads are guarded, including `onRetrySummary`
|
|
92
|
+
* and the failure reporter. Invalid asynchronous results are ignored and never
|
|
93
|
+
* awaited. Observer failures set the log-only `telemetryFailed` marker; the
|
|
94
|
+
* marker and redacted error diagnostics are excluded from headers and tenant
|
|
95
|
+
* metadata. Tenant `meta.retry` retains its existing response shape.
|
|
96
|
+
*
|
|
97
|
+
* After `ProviderEngine.attach`, the server wraps supported host HTTP bindings
|
|
98
|
+
* with the request collector. The adapter preserves arguments, direct-call,
|
|
99
|
+
* explicit `.call`/`.apply`, and unbound receivers, plus the original returned
|
|
100
|
+
* promise, response, stream, and error. Streams and SSE are observed at headers
|
|
101
|
+
* without consuming their bodies. SDK clients report every observed attempt
|
|
102
|
+
* and keep any observer configured here; routing is scoped to each call.
|
|
103
|
+
*
|
|
104
|
+
* Binding an existing adapter reuses its identity and replaces its collector:
|
|
105
|
+
* the latest bind receives subsequent calls, the earlier collector receives no
|
|
106
|
+
* further calls, and in-flight calls keep the collector captured at invocation.
|
|
107
|
+
* An opaque host invocation is one observed attempt with zero observed retries;
|
|
108
|
+
* its undisclosed retries and proxy settings cannot be inferred, and tenant
|
|
109
|
+
* `meta.retry` is omitted. Status, duration, and error class remain available
|
|
110
|
+
* in logs and telemetry headers.
|
|
111
|
+
*
|
|
112
|
+
* Non-object bindings and objects without a callable `request`, `get`, `post`,
|
|
113
|
+
* `put`, `delete`, `stream`, or `sse` method are left untouched, with no `http`
|
|
114
|
+
* telemetry sibling and one process-wide warning for this reason:
|
|
115
|
+
* `[apifuse] http telemetry not attached; reason=unsupported_binding_shape`.
|
|
116
|
+
*/
|
|
117
|
+
httpTelemetry?: HttpTelemetrySink;
|
|
118
|
+
signal?: AbortSignal;
|
|
34
119
|
};
|
|
35
120
|
|
|
36
121
|
type HttpStatusOutcome = {
|
|
@@ -41,19 +126,67 @@ type HttpStatusOutcome = {
|
|
|
41
126
|
proxyUsed: boolean;
|
|
42
127
|
};
|
|
43
128
|
|
|
44
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Sentinel returned when a policy-allocator attempt resolved an endpoint that a
|
|
131
|
+
* prior attempt already tried (an under-filled pool repeats endpoints via the
|
|
132
|
+
* modulo mapping before the flat offset crosses into the next vendor). The retry
|
|
133
|
+
* loop advances to the next offset rather than re-issuing the request — but does
|
|
134
|
+
* NOT treat it as chain exhaustion, so the offset still walks into the fallback
|
|
135
|
+
* vendor's span.
|
|
136
|
+
*/
|
|
137
|
+
type NativeHttpSkipOutcome = { kind: "dedupe-skip" };
|
|
138
|
+
|
|
139
|
+
type NativeHttpAttemptOutcome = HttpResponse | HttpStatusOutcome | NativeHttpSkipOutcome;
|
|
45
140
|
|
|
46
141
|
type NativeHttpAttemptError = TransportError & { proxyUsed?: boolean };
|
|
47
142
|
|
|
48
|
-
function isHttpStatusOutcome(
|
|
49
|
-
outcome: HttpResponse | HttpStatusOutcome,
|
|
50
|
-
): outcome is HttpStatusOutcome {
|
|
143
|
+
function isHttpStatusOutcome(outcome: NativeHttpAttemptOutcome): outcome is HttpStatusOutcome {
|
|
51
144
|
return "kind" in outcome && outcome.kind === "http-status";
|
|
52
145
|
}
|
|
53
146
|
|
|
54
|
-
|
|
147
|
+
function isDedupeSkipOutcome(outcome: NativeHttpAttemptOutcome): outcome is NativeHttpSkipOutcome {
|
|
148
|
+
return "kind" in outcome && outcome.kind === "dedupe-skip";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function toAmbientCancellationError(
|
|
152
|
+
signal: AbortSignal,
|
|
153
|
+
error: unknown = signal.reason,
|
|
154
|
+
): TransportError {
|
|
155
|
+
if (error instanceof TransportError && error.code === "transport_cancelled") {
|
|
156
|
+
return error;
|
|
157
|
+
}
|
|
158
|
+
return new TransportError("Request cancelled", {
|
|
159
|
+
code: "transport_cancelled",
|
|
160
|
+
status: 0,
|
|
161
|
+
retryable: false,
|
|
162
|
+
...(error !== undefined
|
|
163
|
+
? { cause: error instanceof Error ? error : new Error(String(error)) }
|
|
164
|
+
: {}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function throwIfAmbientAborted(signal: AbortSignal | undefined): void {
|
|
169
|
+
if (signal?.aborted) throw toAmbientCancellationError(signal);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
173
|
+
throwIfAmbientAborted(signal);
|
|
55
174
|
if (ms <= 0) return;
|
|
56
|
-
|
|
175
|
+
if (!signal) {
|
|
176
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
await new Promise<void>((resolve, reject) => {
|
|
180
|
+
const onAbort = () => {
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
reject(toAmbientCancellationError(signal));
|
|
183
|
+
};
|
|
184
|
+
const timer = setTimeout(() => {
|
|
185
|
+
signal.removeEventListener("abort", onAbort);
|
|
186
|
+
resolve();
|
|
187
|
+
}, ms);
|
|
188
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
189
|
+
});
|
|
57
190
|
}
|
|
58
191
|
|
|
59
192
|
function toUpstreamHttpError(status: number): TransportError {
|
|
@@ -69,26 +202,108 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
69
202
|
return Object.keys(headers).some((key) => key.toLowerCase() === needle);
|
|
70
203
|
}
|
|
71
204
|
|
|
72
|
-
function
|
|
73
|
-
|
|
205
|
+
function withClientHeaderDefaults(
|
|
206
|
+
headers: Record<string, string> | undefined,
|
|
74
207
|
clientOptions: HttpClientOptions,
|
|
75
208
|
body: unknown,
|
|
76
|
-
):
|
|
77
|
-
const
|
|
209
|
+
): Record<string, string> {
|
|
210
|
+
const merged: Record<string, string> = {
|
|
78
211
|
...(clientOptions.userAgent ? { "User-Agent": clientOptions.userAgent } : {}),
|
|
79
|
-
...
|
|
212
|
+
...headers,
|
|
80
213
|
};
|
|
81
214
|
|
|
82
|
-
if (body !== undefined && !hasHeader(
|
|
83
|
-
|
|
215
|
+
if (body !== undefined && !hasHeader(merged, "Content-Type")) {
|
|
216
|
+
merged["Content-Type"] = "application/json";
|
|
84
217
|
}
|
|
85
218
|
|
|
219
|
+
return merged;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function withClientHeaders(
|
|
223
|
+
options: RequestOptions | undefined,
|
|
224
|
+
clientOptions: HttpClientOptions,
|
|
225
|
+
body: unknown,
|
|
226
|
+
): RequestOptions {
|
|
227
|
+
// The ambient client signal is merged into the per-request one here so every
|
|
228
|
+
// downstream read (retry loop, sleep, body consumption) sees a single signal.
|
|
229
|
+
// The header-factory branch needs it too, otherwise factory-header requests
|
|
230
|
+
// would lose ambient cancellation entirely.
|
|
231
|
+
const signal = mergeAbortSignals(clientOptions.signal, options?.signal);
|
|
232
|
+
// A header factory is resolved per issued attempt (resolveAttemptHeaders);
|
|
233
|
+
// the client defaults are applied to its output at that point.
|
|
234
|
+
if (typeof options?.headers === "function") return { ...options, signal };
|
|
86
235
|
return {
|
|
87
236
|
...options,
|
|
88
|
-
headers,
|
|
237
|
+
headers: withClientHeaderDefaults(options?.headers, clientOptions, body),
|
|
238
|
+
signal,
|
|
89
239
|
};
|
|
90
240
|
}
|
|
91
241
|
|
|
242
|
+
function headerFactoryFailure(message: string, cause?: unknown): TransportError {
|
|
243
|
+
// A dedicated code keeps a broken proof minter out of the retry predicate:
|
|
244
|
+
// toHttpTransportError would otherwise brand a plain throw as
|
|
245
|
+
// transport_network_error, which the default retry policy re-issues.
|
|
246
|
+
// retryable:false is explicit so the served error envelope does not
|
|
247
|
+
// advertise a deterministic signing failure as retryable, and
|
|
248
|
+
// category:provider_error keeps a provider-local minting bug out of the
|
|
249
|
+
// upstream-failure signal (status 0 would otherwise fall through the
|
|
250
|
+
// observability ladder to upstream_http / source upstream_failure).
|
|
251
|
+
return new TransportError(message, {
|
|
252
|
+
code: "http_header_factory_failed",
|
|
253
|
+
status: 0,
|
|
254
|
+
retryable: false,
|
|
255
|
+
category: "provider_error",
|
|
256
|
+
...(cause instanceof Error ? { cause } : {}),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function isHeaderRecord(value: unknown): value is Record<string, string> {
|
|
261
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Settle `promise`, or reject with the signal's reason as soon as it aborts, so
|
|
265
|
+
// a pending header factory cannot outlive the request deadline or an ambient
|
|
266
|
+
// cancellation (native fetch has not started yet, so nothing else observes them).
|
|
267
|
+
function settleBeforeAbort<T>(promise: Promise<T>, signal: AbortSignal | undefined): Promise<T> {
|
|
268
|
+
if (!signal) return promise;
|
|
269
|
+
return new Promise<T>((resolve, reject) => {
|
|
270
|
+
const onAbort = () => reject(signal.reason);
|
|
271
|
+
if (signal.aborted) onAbort();
|
|
272
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
273
|
+
// Always attach handlers: a late rejection after the abort must settle
|
|
274
|
+
// here (a no-op on the already-rejected promise), never go unhandled.
|
|
275
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function resolveAttemptHeaders(
|
|
280
|
+
options: RequestOptions & { body?: unknown },
|
|
281
|
+
clientOptions: HttpClientOptions,
|
|
282
|
+
attempt: () => HttpAttemptContext,
|
|
283
|
+
signal: AbortSignal | undefined,
|
|
284
|
+
): Promise<Record<string, string> | undefined> {
|
|
285
|
+
const { headers } = options;
|
|
286
|
+
if (typeof headers !== "function") return headers;
|
|
287
|
+
// The per-call timeout may have fired during proxy resolution: do not start
|
|
288
|
+
// the factory at all once the request is already aborted.
|
|
289
|
+
if (signal?.aborted) throw signal.reason;
|
|
290
|
+
let minted: unknown;
|
|
291
|
+
try {
|
|
292
|
+
minted = await settleBeforeAbort(
|
|
293
|
+
Promise.resolve().then(() => headers(attempt())),
|
|
294
|
+
signal,
|
|
295
|
+
);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
// An abort is classified by toHttpTransportError (cancelled / timed out).
|
|
298
|
+
if (signal?.aborted) throw error;
|
|
299
|
+
throw headerFactoryFailure("Request header factory failed", error);
|
|
300
|
+
}
|
|
301
|
+
if (!isHeaderRecord(minted)) {
|
|
302
|
+
throw headerFactoryFailure("Request header factory must return a header record");
|
|
303
|
+
}
|
|
304
|
+
return withClientHeaderDefaults(minted, clientOptions, options.body);
|
|
305
|
+
}
|
|
306
|
+
|
|
92
307
|
function parseHttpData(body: string, headers: Record<string, string>): unknown {
|
|
93
308
|
const contentType = headers["content-type"] ?? headers["Content-Type"] ?? headers["CONTENT-TYPE"];
|
|
94
309
|
|
|
@@ -107,7 +322,21 @@ function isTimeoutMessage(message: string): boolean {
|
|
|
107
322
|
return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
|
|
108
323
|
}
|
|
109
324
|
|
|
110
|
-
function toHttpTransportError(
|
|
325
|
+
function toHttpTransportError(
|
|
326
|
+
error: unknown,
|
|
327
|
+
ambientSignal?: AbortSignal,
|
|
328
|
+
timeoutSignal?: AbortSignal,
|
|
329
|
+
): TransportError {
|
|
330
|
+
if (ambientSignal?.aborted) {
|
|
331
|
+
return toAmbientCancellationError(ambientSignal, error);
|
|
332
|
+
}
|
|
333
|
+
if (timeoutSignal?.aborted) {
|
|
334
|
+
return new TransportError("Request timed out", {
|
|
335
|
+
code: "transport_timeout",
|
|
336
|
+
status: 0,
|
|
337
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
338
|
+
});
|
|
339
|
+
}
|
|
111
340
|
if (error instanceof TransportError) {
|
|
112
341
|
if (error.code) {
|
|
113
342
|
return error;
|
|
@@ -191,9 +420,134 @@ function requireNativeResponseBody(response: Response): ReadableStream<Uint8Arra
|
|
|
191
420
|
return response.body;
|
|
192
421
|
}
|
|
193
422
|
|
|
194
|
-
function
|
|
423
|
+
function mergeAbortSignals(
|
|
424
|
+
...signals: Array<AbortSignal | null | undefined>
|
|
425
|
+
): AbortSignal | undefined {
|
|
426
|
+
const activeSignals = signals.filter((signal): signal is AbortSignal => signal != null);
|
|
427
|
+
if (activeSignals.length === 0) return undefined;
|
|
428
|
+
if (activeSignals.length === 1) return activeSignals[0];
|
|
429
|
+
return AbortSignal.any(activeSignals);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function cancelStreamOnAbort(
|
|
433
|
+
body: ReadableStream<Uint8Array>,
|
|
434
|
+
signal: AbortSignal | undefined,
|
|
435
|
+
): ReadableStream<Uint8Array> {
|
|
436
|
+
if (!signal) return body;
|
|
437
|
+
|
|
438
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
439
|
+
let finished = false;
|
|
440
|
+
let streamController: ReadableStreamDefaultController<Uint8Array> | undefined;
|
|
441
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
442
|
+
const onAbort = () => {
|
|
443
|
+
if (finished) return;
|
|
444
|
+
finished = true;
|
|
445
|
+
cleanup();
|
|
446
|
+
const reason = toAmbientCancellationError(signal);
|
|
447
|
+
streamController?.error(reason);
|
|
448
|
+
const cancellation = reader ? reader.cancel(reason) : body.cancel(reason);
|
|
449
|
+
void cancellation.catch(() => undefined).finally(() => reader?.releaseLock());
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
return new ReadableStream<Uint8Array>(
|
|
453
|
+
{
|
|
454
|
+
start(controller) {
|
|
455
|
+
streamController = controller;
|
|
456
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
457
|
+
if (signal.aborted) onAbort();
|
|
458
|
+
},
|
|
459
|
+
async pull(controller) {
|
|
460
|
+
try {
|
|
461
|
+
reader ??= body.getReader();
|
|
462
|
+
const chunk = await reader.read();
|
|
463
|
+
if (finished) return;
|
|
464
|
+
if (chunk.done) {
|
|
465
|
+
finished = true;
|
|
466
|
+
cleanup();
|
|
467
|
+
controller.close();
|
|
468
|
+
reader.releaseLock();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
controller.enqueue(chunk.value);
|
|
472
|
+
} catch (error) {
|
|
473
|
+
if (finished) return;
|
|
474
|
+
finished = true;
|
|
475
|
+
cleanup();
|
|
476
|
+
controller.error(error);
|
|
477
|
+
reader?.releaseLock();
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
async cancel(reason) {
|
|
481
|
+
if (finished) return;
|
|
482
|
+
finished = true;
|
|
483
|
+
cleanup();
|
|
484
|
+
try {
|
|
485
|
+
await (reader ? reader.cancel(reason) : body.cancel(reason));
|
|
486
|
+
} finally {
|
|
487
|
+
reader?.releaseLock();
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
{ highWaterMark: 0 },
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function sanitizeStreamErrors(
|
|
496
|
+
body: ReadableStream<Uint8Array>,
|
|
497
|
+
serializedUrl: SerializedRequestUrl,
|
|
498
|
+
): ReadableStream<Uint8Array> {
|
|
499
|
+
if (serializedUrl.sensitiveValues.length === 0) return body;
|
|
500
|
+
|
|
501
|
+
let reader: ReadableStreamDefaultReader<Uint8Array> | undefined;
|
|
502
|
+
return new ReadableStream<Uint8Array>(
|
|
503
|
+
{
|
|
504
|
+
async pull(controller) {
|
|
505
|
+
try {
|
|
506
|
+
reader ??= body.getReader();
|
|
507
|
+
const chunk = await reader.read();
|
|
508
|
+
if (chunk.done) {
|
|
509
|
+
controller.close();
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
controller.enqueue(chunk.value);
|
|
513
|
+
} catch (error) {
|
|
514
|
+
controller.error(
|
|
515
|
+
redactSensitiveError(
|
|
516
|
+
error,
|
|
517
|
+
serializedUrl.sensitiveValues,
|
|
518
|
+
serializedUrl.requestUrl,
|
|
519
|
+
serializedUrl.redactedUrl,
|
|
520
|
+
),
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
async cancel(reason) {
|
|
525
|
+
try {
|
|
526
|
+
await (reader ? reader.cancel(reason) : body.cancel(reason));
|
|
527
|
+
} catch (error) {
|
|
528
|
+
throw redactSensitiveError(
|
|
529
|
+
error,
|
|
530
|
+
serializedUrl.sensitiveValues,
|
|
531
|
+
serializedUrl.requestUrl,
|
|
532
|
+
serializedUrl.redactedUrl,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
{ highWaterMark: 0 },
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function toNativeHttpStreamResponse(
|
|
542
|
+
response: Response,
|
|
543
|
+
serializedUrl: SerializedRequestUrl,
|
|
544
|
+
signal?: AbortSignal,
|
|
545
|
+
): HttpStreamResponse {
|
|
195
546
|
const headers = Object.fromEntries(response.headers.entries());
|
|
196
|
-
const body =
|
|
547
|
+
const body = sanitizeStreamErrors(
|
|
548
|
+
cancelStreamOnAbort(requireNativeResponseBody(response), signal),
|
|
549
|
+
serializedUrl,
|
|
550
|
+
);
|
|
197
551
|
return {
|
|
198
552
|
body,
|
|
199
553
|
headers,
|
|
@@ -205,7 +559,7 @@ function toNativeHttpStreamResponse(response: Response): HttpStreamResponse {
|
|
|
205
559
|
};
|
|
206
560
|
}
|
|
207
561
|
|
|
208
|
-
function normalizeHttpMethod(method: string): HttpMethod {
|
|
562
|
+
function normalizeHttpMethod(method: string): Uppercase<HttpMethod> {
|
|
209
563
|
switch (method.toUpperCase()) {
|
|
210
564
|
case "HEAD":
|
|
211
565
|
return "HEAD";
|
|
@@ -230,7 +584,8 @@ function normalizeHttpMethod(method: string): HttpMethod {
|
|
|
230
584
|
}
|
|
231
585
|
}
|
|
232
586
|
|
|
233
|
-
|
|
587
|
+
/** Whether `url` carries its own scheme, so it needs no `upstream.baseUrl` to resolve. */
|
|
588
|
+
export function isAbsoluteUrl(url: string): boolean {
|
|
234
589
|
return /^[a-z][a-z\d+\-.]*:/i.test(url);
|
|
235
590
|
}
|
|
236
591
|
|
|
@@ -240,6 +595,105 @@ function resolveHttpUrl(baseUrl: string | undefined, url: string): string {
|
|
|
240
595
|
|
|
241
596
|
type NativeFetchInit = RequestInit & { proxy?: string };
|
|
242
597
|
|
|
598
|
+
const MAX_HTTP_REDIRECT_HOPS = 20;
|
|
599
|
+
const HTTP_REDIRECT_POLICY_TRANSPORT: RedirectPolicyTransport = {
|
|
600
|
+
label: "ctx.http",
|
|
601
|
+
maxHops: MAX_HTTP_REDIRECT_HOPS,
|
|
602
|
+
};
|
|
603
|
+
const REDIRECT_BODY_HEADERS = new Set([
|
|
604
|
+
"content-encoding",
|
|
605
|
+
"content-language",
|
|
606
|
+
"content-location",
|
|
607
|
+
"content-type",
|
|
608
|
+
]);
|
|
609
|
+
|
|
610
|
+
function snapshotHttpRedirectPolicy(options: RequestOptions): HttpRedirectPolicy | undefined {
|
|
611
|
+
try {
|
|
612
|
+
return normalizeRedirectPolicy(options.redirectPolicy, HTTP_REDIRECT_POLICY_TRANSPORT);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
if (error instanceof TransportError) throw error;
|
|
615
|
+
throw invalidRedirectPolicy(
|
|
616
|
+
HTTP_REDIRECT_POLICY_TRANSPORT,
|
|
617
|
+
"could not be read safely",
|
|
618
|
+
error instanceof Error ? error : undefined,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function withoutRedirectBodyHeaders(headers: HeadersInit | undefined): Headers {
|
|
624
|
+
const nextHeaders = new Headers(headers);
|
|
625
|
+
for (const name of REDIRECT_BODY_HEADERS) nextHeaders.delete(name);
|
|
626
|
+
return nextHeaders;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function discardRedirectResponseBody(response: Response): void {
|
|
630
|
+
try {
|
|
631
|
+
const cancellation = response.body?.cancel();
|
|
632
|
+
if (cancellation) void cancellation.catch(() => undefined);
|
|
633
|
+
} catch {
|
|
634
|
+
// The redirect decision is security-significant and must not be replaced
|
|
635
|
+
// or delayed by an upstream body's cancellation failure. Cancellation was
|
|
636
|
+
// attempted; redirect evaluation continues without awaiting its completion.
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
async function fetchWithHttpRedirectPolicy(
|
|
641
|
+
requestUrl: string,
|
|
642
|
+
requestInit: NativeFetchInit,
|
|
643
|
+
policy: HttpRedirectPolicy | undefined,
|
|
644
|
+
): Promise<Response> {
|
|
645
|
+
if (!policy) return fetch(requestUrl, requestInit);
|
|
646
|
+
|
|
647
|
+
const initialUrl = new URL(requestUrl);
|
|
648
|
+
if (initialUrl.protocol !== "http:" && initialUrl.protocol !== "https:") {
|
|
649
|
+
throw new TransportError("ctx.http redirectPolicy requires an HTTP(S) origin", {
|
|
650
|
+
code: "transport_invalid_url",
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const initialOrigin = initialUrl.origin;
|
|
655
|
+
let currentUrl = requestUrl;
|
|
656
|
+
let method = normalizeHttpMethod(requestInit.method ?? "GET");
|
|
657
|
+
let body = requestInit.body;
|
|
658
|
+
let headers = requestInit.headers;
|
|
659
|
+
let followedHops = 0;
|
|
660
|
+
const visitedRequests = new Set([`${method} ${currentUrl}`]);
|
|
661
|
+
|
|
662
|
+
while (true) {
|
|
663
|
+
const response = await fetch(currentUrl, {
|
|
664
|
+
...requestInit,
|
|
665
|
+
body,
|
|
666
|
+
headers,
|
|
667
|
+
method,
|
|
668
|
+
redirect: "manual",
|
|
669
|
+
});
|
|
670
|
+
if (!isRedirectStatus(response.status)) return response;
|
|
671
|
+
|
|
672
|
+
const location = response.headers.get("location");
|
|
673
|
+
discardRedirectResponseBody(response);
|
|
674
|
+
const decision = nextSameOriginRedirectHop({
|
|
675
|
+
policy,
|
|
676
|
+
initialOrigin,
|
|
677
|
+
currentUrl,
|
|
678
|
+
responseUrl: currentUrl,
|
|
679
|
+
status: response.status,
|
|
680
|
+
method,
|
|
681
|
+
location: location ?? undefined,
|
|
682
|
+
followedHops,
|
|
683
|
+
visitedRequests,
|
|
684
|
+
});
|
|
685
|
+
if (decision.nextMethod !== method) {
|
|
686
|
+
body = undefined;
|
|
687
|
+
headers = withoutRedirectBodyHeaders(headers);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
method = decision.nextMethod;
|
|
691
|
+
currentUrl = decision.nextUrl;
|
|
692
|
+
followedHops += 1;
|
|
693
|
+
visitedRequests.add(`${method} ${currentUrl}`);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
243
697
|
async function resolveNativeProxy(
|
|
244
698
|
options: RequestOptions,
|
|
245
699
|
clientOptions: HttpClientOptions,
|
|
@@ -249,14 +703,17 @@ async function resolveNativeProxy(
|
|
|
249
703
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
250
704
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
251
705
|
upstream: clientOptions.upstream,
|
|
252
|
-
apifuseConfig: clientOptions.apifuseConfig,
|
|
253
706
|
proxyPolicy: clientOptions.proxyPolicy,
|
|
254
707
|
affinityKey: clientOptions.affinityKey,
|
|
255
708
|
proxyAttempt: computeProxyAttemptIndex({
|
|
256
709
|
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
257
710
|
retryAttemptOffset: proxyAttemptOffset,
|
|
258
711
|
}),
|
|
712
|
+
// Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
|
|
713
|
+
// supported here, so a socks5 policy fails loudly rather than downgrading.
|
|
714
|
+
transportProtocols: ["http"],
|
|
259
715
|
telemetry: clientOptions.telemetry,
|
|
716
|
+
engineCredentials: clientOptions.engineCredentials,
|
|
260
717
|
});
|
|
261
718
|
if (resolvedProxy.shouldWarn) {
|
|
262
719
|
warn(
|
|
@@ -285,37 +742,91 @@ function normalizeNativeFetchBody(body: unknown): string | ArrayBuffer | undefin
|
|
|
285
742
|
return copied.buffer;
|
|
286
743
|
}
|
|
287
744
|
|
|
745
|
+
function serializeHttpRequestUrl(
|
|
746
|
+
baseUrl: string | undefined,
|
|
747
|
+
url: string,
|
|
748
|
+
options: RequestOptions,
|
|
749
|
+
): SerializedRequestUrl {
|
|
750
|
+
try {
|
|
751
|
+
return serializeRequestUrl(
|
|
752
|
+
resolveHttpUrl(baseUrl, url),
|
|
753
|
+
options.params,
|
|
754
|
+
options.sensitiveParams,
|
|
755
|
+
);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
288
761
|
async function fetchNativeHttp(
|
|
289
762
|
baseUrl: string | undefined,
|
|
290
763
|
url: string,
|
|
291
|
-
method: HttpMethod
|
|
764
|
+
method: Uppercase<HttpMethod>,
|
|
292
765
|
options: RequestOptions & { body?: unknown },
|
|
293
766
|
clientOptions: HttpClientOptions,
|
|
294
767
|
warn: (message: string) => void,
|
|
295
768
|
statusRetryCodes?: readonly number[],
|
|
296
769
|
proxyAttemptOffset = 0,
|
|
770
|
+
dedupe?: { attempted: Set<string> },
|
|
771
|
+
observeProxy?: (used: boolean, status?: number) => void,
|
|
772
|
+
nextHeaderAttempt: () => number = () => 1,
|
|
297
773
|
): Promise<NativeHttpAttemptOutcome> {
|
|
298
|
-
const
|
|
774
|
+
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
775
|
+
const { requestUrl } = serializedUrl;
|
|
299
776
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
300
|
-
const
|
|
777
|
+
const signal = mergeAbortSignals(options.signal, controller?.signal);
|
|
778
|
+
let timeoutHandle = options.timeout
|
|
301
779
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
302
780
|
: undefined;
|
|
781
|
+
const disarmTimeout = () => {
|
|
782
|
+
if (!timeoutHandle) return;
|
|
783
|
+
clearTimeout(timeoutHandle);
|
|
784
|
+
timeoutHandle = undefined;
|
|
785
|
+
};
|
|
303
786
|
|
|
304
787
|
let proxy: string | undefined;
|
|
305
788
|
try {
|
|
789
|
+
throwIfAmbientAborted(options.signal);
|
|
790
|
+
// Resolve inside the try (and after the timeout is armed) so allocator
|
|
791
|
+
// failures are branded as TransportErrors and count against the request
|
|
792
|
+
// deadline, exactly as an inline resolve would.
|
|
306
793
|
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
794
|
+
observeProxy?.(Boolean(proxy));
|
|
795
|
+
throwIfAmbientAborted(options.signal);
|
|
796
|
+
// For a registry allocator chain, skip an endpoint a prior attempt already
|
|
797
|
+
// tried rather than re-issuing the same request. Returning the sentinel
|
|
798
|
+
// (instead of breaking) lets the loop keep advancing the flat offset until
|
|
799
|
+
// it crosses into the fallback vendor's pool span.
|
|
800
|
+
if (dedupe && proxy) {
|
|
801
|
+
if (dedupe.attempted.has(proxy)) {
|
|
802
|
+
return { kind: "dedupe-skip" };
|
|
803
|
+
}
|
|
804
|
+
dedupe.attempted.add(proxy);
|
|
805
|
+
}
|
|
806
|
+
// Resolve after the dedupe check so a skipped duplicate offset never burns
|
|
807
|
+
// a single-use proof, and with the serialized URL so URL-bound proofs
|
|
808
|
+
// match what is actually issued.
|
|
809
|
+
const requestHeaders = await resolveAttemptHeaders(
|
|
810
|
+
options,
|
|
811
|
+
clientOptions,
|
|
812
|
+
() => ({ attempt: nextHeaderAttempt(), url: requestUrl, method }),
|
|
813
|
+
signal,
|
|
814
|
+
);
|
|
307
815
|
const requestInit: NativeFetchInit = {
|
|
308
|
-
headers:
|
|
816
|
+
headers: requestHeaders,
|
|
309
817
|
method,
|
|
310
818
|
...(proxy ? { proxy } : {}),
|
|
311
|
-
signal:
|
|
819
|
+
...(signal ? { signal } : {}),
|
|
312
820
|
};
|
|
313
821
|
if (options.body !== undefined) {
|
|
314
822
|
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
315
823
|
}
|
|
316
|
-
const response = await
|
|
317
|
-
|
|
318
|
-
|
|
824
|
+
const response = await fetchWithHttpRedirectPolicy(
|
|
825
|
+
requestUrl,
|
|
826
|
+
requestInit,
|
|
827
|
+
options.redirectPolicy,
|
|
828
|
+
);
|
|
829
|
+
observeProxy?.(Boolean(proxy), response.status);
|
|
319
830
|
const headers = Object.fromEntries(response.headers.entries());
|
|
320
831
|
|
|
321
832
|
if (statusRetryCodes && response.status >= 400) {
|
|
@@ -337,47 +848,80 @@ async function fetchNativeHttp(
|
|
|
337
848
|
});
|
|
338
849
|
}
|
|
339
850
|
|
|
340
|
-
|
|
851
|
+
// `timeout` bounds the header phase (types.ts RequestOptions.timeout), so it
|
|
852
|
+
// is disarmed before the body is read: awaiting the buffered body inside
|
|
853
|
+
// this try brands a mid-body failure as a transport error, but must not
|
|
854
|
+
// extend the deadline over the download. Cancellation still applies —
|
|
855
|
+
// `signal` stays attached to the native response. The discarded non-2xx
|
|
856
|
+
// bodies drained above stay under the deadline on purpose: nothing else
|
|
857
|
+
// bounds them, and their content is never returned to the caller.
|
|
858
|
+
disarmTimeout();
|
|
859
|
+
return await toNativeHttpResponse(response);
|
|
341
860
|
} catch (error) {
|
|
342
861
|
if (error instanceof SyntaxError) {
|
|
343
|
-
throw
|
|
862
|
+
throw redactSensitiveError(
|
|
863
|
+
error,
|
|
864
|
+
serializedUrl.sensitiveValues,
|
|
865
|
+
serializedUrl.requestUrl,
|
|
866
|
+
serializedUrl.redactedUrl,
|
|
867
|
+
);
|
|
344
868
|
}
|
|
345
|
-
const transportError =
|
|
869
|
+
const transportError: NativeHttpAttemptError = redactSensitiveError(
|
|
870
|
+
toHttpTransportError(error, options.signal, controller?.signal),
|
|
871
|
+
serializedUrl.sensitiveValues,
|
|
872
|
+
serializedUrl.requestUrl,
|
|
873
|
+
serializedUrl.redactedUrl,
|
|
874
|
+
);
|
|
346
875
|
transportError.proxyUsed = Boolean(proxy);
|
|
347
876
|
throw transportError;
|
|
348
877
|
} finally {
|
|
349
|
-
|
|
878
|
+
disarmTimeout();
|
|
350
879
|
}
|
|
351
880
|
}
|
|
352
881
|
|
|
353
882
|
async function fetchNativeHttpStream(
|
|
354
883
|
baseUrl: string | undefined,
|
|
355
884
|
url: string,
|
|
356
|
-
method: HttpMethod
|
|
885
|
+
method: Uppercase<HttpMethod>,
|
|
357
886
|
options: RequestOptions & { body?: unknown },
|
|
358
887
|
clientOptions: HttpClientOptions,
|
|
359
888
|
warn: (message: string) => void,
|
|
889
|
+
observeProxy?: (used: boolean) => void,
|
|
360
890
|
): Promise<HttpStreamResponse> {
|
|
361
|
-
const
|
|
891
|
+
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
892
|
+
const { requestUrl } = serializedUrl;
|
|
362
893
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
894
|
+
const signal = mergeAbortSignals(options.signal, controller?.signal);
|
|
363
895
|
const timeoutHandle = options.timeout
|
|
364
896
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
365
897
|
: undefined;
|
|
366
898
|
|
|
367
899
|
try {
|
|
900
|
+
throwIfAmbientAborted(options.signal);
|
|
368
901
|
const proxy = await resolveNativeProxy(options, clientOptions, warn);
|
|
902
|
+
observeProxy?.(Boolean(proxy));
|
|
903
|
+
throwIfAmbientAborted(options.signal);
|
|
904
|
+
// Streams never retry: a header factory is resolved once, as attempt 1.
|
|
905
|
+
const headers = await resolveAttemptHeaders(
|
|
906
|
+
options,
|
|
907
|
+
clientOptions,
|
|
908
|
+
() => ({ attempt: 1, url: requestUrl, method }),
|
|
909
|
+
signal,
|
|
910
|
+
);
|
|
369
911
|
const requestInit: NativeFetchInit = {
|
|
370
|
-
headers
|
|
912
|
+
headers,
|
|
371
913
|
method,
|
|
372
914
|
...(proxy ? { proxy } : {}),
|
|
373
|
-
signal:
|
|
915
|
+
...(signal ? { signal } : {}),
|
|
374
916
|
};
|
|
375
917
|
if (options.body !== undefined) {
|
|
376
918
|
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
377
919
|
}
|
|
378
|
-
const response = await
|
|
379
|
-
|
|
380
|
-
|
|
920
|
+
const response = await fetchWithHttpRedirectPolicy(
|
|
921
|
+
requestUrl,
|
|
922
|
+
requestInit,
|
|
923
|
+
options.redirectPolicy,
|
|
924
|
+
);
|
|
381
925
|
|
|
382
926
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
383
927
|
await drainNativeResponseBody(response);
|
|
@@ -387,12 +931,24 @@ async function fetchNativeHttpStream(
|
|
|
387
931
|
});
|
|
388
932
|
}
|
|
389
933
|
|
|
390
|
-
|
|
934
|
+
// The stream timeout remains header-scoped; composed ambient and local
|
|
935
|
+
// cancellation stays attached to native fetch for the body's full lifetime.
|
|
936
|
+
return toNativeHttpStreamResponse(response, serializedUrl, options.signal);
|
|
391
937
|
} catch (error) {
|
|
392
938
|
if (error instanceof SyntaxError) {
|
|
393
|
-
throw
|
|
939
|
+
throw redactSensitiveError(
|
|
940
|
+
error,
|
|
941
|
+
serializedUrl.sensitiveValues,
|
|
942
|
+
serializedUrl.requestUrl,
|
|
943
|
+
serializedUrl.redactedUrl,
|
|
944
|
+
);
|
|
394
945
|
}
|
|
395
|
-
throw
|
|
946
|
+
throw redactSensitiveError(
|
|
947
|
+
toHttpTransportError(error, options.signal, controller?.signal),
|
|
948
|
+
serializedUrl.sensitiveValues,
|
|
949
|
+
serializedUrl.requestUrl,
|
|
950
|
+
serializedUrl.redactedUrl,
|
|
951
|
+
);
|
|
396
952
|
} finally {
|
|
397
953
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
398
954
|
}
|
|
@@ -417,113 +973,257 @@ export function createHttpClient(
|
|
|
417
973
|
method: string,
|
|
418
974
|
options: RequestOptions & { body?: unknown } = {},
|
|
419
975
|
): Promise<HttpResponse> {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
976
|
+
const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
|
|
977
|
+
try {
|
|
978
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
979
|
+
throw new TransportError(
|
|
980
|
+
"ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
|
|
981
|
+
{ code: "transport_invalid_url" },
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
assertNoHttpTransportOverrides(options);
|
|
985
|
+
const redirectPolicy = snapshotHttpRedirectPolicy(options);
|
|
986
|
+
const headersOptions = {
|
|
987
|
+
...withClientHeaders(options, clientOptions, options.body),
|
|
988
|
+
redirectPolicy,
|
|
989
|
+
};
|
|
990
|
+
const methodName = normalizeHttpMethod(method);
|
|
991
|
+
const explicitRetry = headersOptions.retry !== undefined;
|
|
992
|
+
const retryOptions =
|
|
993
|
+
normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
994
|
+
label: "HTTP",
|
|
995
|
+
}) ??
|
|
996
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
997
|
+
if (retryOptions) validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
998
|
+
return { explicitRetry, headersOptions, methodName, retryOptions };
|
|
999
|
+
} catch (error) {
|
|
1000
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
1001
|
+
}
|
|
1002
|
+
})();
|
|
1003
|
+
const requestStarted = performance.now();
|
|
1004
|
+
const telemetry = startHttpTelemetry(
|
|
1005
|
+
scopedHttpTelemetry(clientOptions.httpTelemetry),
|
|
1006
|
+
retryOptions?.preset,
|
|
447
1007
|
);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
1008
|
+
try {
|
|
1009
|
+
return await runRequest();
|
|
1010
|
+
} finally {
|
|
1011
|
+
telemetry.finish(performance.now() - requestStarted);
|
|
1012
|
+
}
|
|
451
1013
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
attemptOptions,
|
|
458
|
-
clientOptions,
|
|
459
|
-
warnOnce,
|
|
460
|
-
statusRetryEnabled ? retryOptions?.statusCodes : undefined,
|
|
461
|
-
proxyAttemptOffset,
|
|
1014
|
+
async function runRequest(): Promise<HttpResponse> {
|
|
1015
|
+
const retryEnabled = Boolean(
|
|
1016
|
+
retryOptions &&
|
|
1017
|
+
retryOptions.attempts > 1 &&
|
|
1018
|
+
isProxyTransportRetryMethod(methodName, retryOptions),
|
|
462
1019
|
);
|
|
1020
|
+
const statusRetryEnabled = Boolean(
|
|
1021
|
+
retryEnabled &&
|
|
1022
|
+
explicitRetry &&
|
|
1023
|
+
retryOptions &&
|
|
1024
|
+
retryOptions.statusCodes.length > 0 &&
|
|
1025
|
+
headersOptions.throwOnHttpError !== false,
|
|
1026
|
+
);
|
|
1027
|
+
const attemptOptions: RequestOptions & { body?: unknown } = statusRetryEnabled
|
|
1028
|
+
? { ...headersOptions, throwOnHttpError: false }
|
|
1029
|
+
: headersOptions;
|
|
463
1030
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
1031
|
+
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
1032
|
+
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
1033
|
+
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
1034
|
+
// spans), so a transport failure should advance to the next endpoint —
|
|
1035
|
+
// potentially crossing into the fallback vendor — rather than stopping at
|
|
1036
|
+
// the per-endpoint retry budget and stranding the request on the primary
|
|
1037
|
+
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
1038
|
+
// only for implicit, safe-method allocator requests; explicit retry
|
|
1039
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
1040
|
+
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
1041
|
+
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
1042
|
+
// the full span.
|
|
1043
|
+
const policyProxy: ProviderProxyPolicy | undefined = (() => {
|
|
1044
|
+
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
1045
|
+
return policy && typeof policy === "object" ? policy : undefined;
|
|
1046
|
+
})();
|
|
1047
|
+
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
1048
|
+
const transportAttemptCap = retryOptions
|
|
1049
|
+
? resolvePolicyTransportAttemptCap({
|
|
1050
|
+
policy: policyProxy,
|
|
1051
|
+
usesPolicyAllocator,
|
|
1052
|
+
retryAttempts: retryOptions.attempts,
|
|
1053
|
+
explicitRetry,
|
|
1054
|
+
method: methodName,
|
|
1055
|
+
})
|
|
1056
|
+
: 1;
|
|
471
1057
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1058
|
+
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
1059
|
+
// attempts rotate the flat offset across the concatenated vendor pool
|
|
1060
|
+
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
1061
|
+
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
1062
|
+
// the offset reaches the next vendor. Rather than re-hammering an
|
|
1063
|
+
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
1064
|
+
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
1065
|
+
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
1066
|
+
// vendor's pool span instead of stalling on the primary vendor.
|
|
1067
|
+
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
1068
|
+
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
1069
|
+
// an explicit retry policy: there the caller's `attempts` count is the
|
|
1070
|
+
// contract and each attempt must issue against whatever endpoint it resolves
|
|
1071
|
+
// — even a repeat — instead of being silently skipped (which would collapse
|
|
1072
|
+
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
1073
|
+
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
1074
|
+
policy: policyProxy,
|
|
1075
|
+
usesPolicyAllocator,
|
|
1076
|
+
explicitRetry,
|
|
1077
|
+
method: methodName,
|
|
1078
|
+
});
|
|
1079
|
+
const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set<string>() } : undefined;
|
|
1080
|
+
|
|
1081
|
+
// Numbers the attempts a header factory sees: only requests that reached
|
|
1082
|
+
// header resolution count, so a failed proxy allocation (which increments
|
|
1083
|
+
// the telemetry `issued` count below) or a dedupe-skipped offset does not
|
|
1084
|
+
// make the first request that is actually built report `attempt: 2`.
|
|
1085
|
+
let headerAttempts = 0;
|
|
1086
|
+
const nextHeaderAttempt = () => {
|
|
1087
|
+
headerAttempts += 1;
|
|
1088
|
+
return headerAttempts;
|
|
1089
|
+
};
|
|
1090
|
+
|
|
1091
|
+
const executeOnce = async (proxyAttemptOffset = 0): Promise<NativeHttpAttemptOutcome> => {
|
|
1092
|
+
const started = performance.now();
|
|
1093
|
+
let proxyUsed = false;
|
|
1094
|
+
let observedStatus: number | undefined;
|
|
1095
|
+
const record = (event: Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed">) => {
|
|
1096
|
+
telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
|
|
1097
|
+
};
|
|
1098
|
+
try {
|
|
1099
|
+
const outcome = await fetchNativeHttp(
|
|
1100
|
+
baseUrl,
|
|
1101
|
+
url,
|
|
1102
|
+
methodName,
|
|
1103
|
+
attemptOptions,
|
|
1104
|
+
clientOptions,
|
|
1105
|
+
warnOnce,
|
|
1106
|
+
statusRetryEnabled ? retryOptions?.statusCodes : undefined,
|
|
1107
|
+
proxyAttemptOffset,
|
|
1108
|
+
dedupeContext,
|
|
1109
|
+
(used, status) => {
|
|
1110
|
+
proxyUsed = used;
|
|
1111
|
+
observedStatus = status;
|
|
1112
|
+
},
|
|
1113
|
+
nextHeaderAttempt,
|
|
1114
|
+
);
|
|
1115
|
+
if (!isDedupeSkipOutcome(outcome))
|
|
1116
|
+
record({
|
|
1117
|
+
status: outcome.status,
|
|
1118
|
+
...(isHttpStatusOutcome(outcome)
|
|
1119
|
+
? { e: "upstream_http_error", statusRetry: true }
|
|
1120
|
+
: {}),
|
|
1121
|
+
});
|
|
1122
|
+
return outcome;
|
|
1123
|
+
} catch (error) {
|
|
1124
|
+
// Record before the retry loop can catch/rethrow a terminal status.
|
|
1125
|
+
record(httpAttemptFailure(error, observedStatus));
|
|
1126
|
+
throw error;
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
if (!retryEnabled || !retryOptions) {
|
|
1131
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
1132
|
+
const outcome = await executeOnce();
|
|
1133
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
1134
|
+
// Single-shot path never de-duplicates (dedupeContext is undefined),
|
|
1135
|
+
// but keep the union total.
|
|
1136
|
+
throw new TransportError("HTTP request produced no terminal result", {
|
|
1137
|
+
code: "retry_exhausted",
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
477
1140
|
if (isHttpStatusOutcome(outcome)) {
|
|
478
|
-
lastStatus = outcome.status;
|
|
479
|
-
if (outcome.retryable && attempt < retryOptions.attempts) {
|
|
480
|
-
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
1141
|
throw toUpstreamHttpError(outcome.status);
|
|
484
1142
|
}
|
|
1143
|
+
return outcome;
|
|
1144
|
+
}
|
|
485
1145
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1146
|
+
let lastError: unknown;
|
|
1147
|
+
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
1148
|
+
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
1149
|
+
// increment it). Retry summaries and the status-retry budget must reflect
|
|
1150
|
+
// issued requests, not the raw offset, so they stay accurate when partial
|
|
1151
|
+
// allocations skip offsets.
|
|
1152
|
+
let issued = 0;
|
|
1153
|
+
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
1154
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
1155
|
+
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
1156
|
+
// so the catch counts a thrown *transport* failure once without
|
|
1157
|
+
// double-counting a status outcome that already incremented before it
|
|
1158
|
+
// re-threw as an upstream HTTP error.
|
|
1159
|
+
let issuedThisAttempt = false;
|
|
1160
|
+
try {
|
|
1161
|
+
const outcome = await executeOnce(attempt - 1);
|
|
1162
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
1163
|
+
// Duplicate endpoint from a partial allocation: advance the flat
|
|
1164
|
+
// offset without issuing the request (no backoff, not a failure) so
|
|
1165
|
+
// the loop keeps rotating toward the fallback vendor.
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
issued += 1;
|
|
1169
|
+
issuedThisAttempt = true;
|
|
1170
|
+
if (isHttpStatusOutcome(outcome)) {
|
|
1171
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
1172
|
+
await sleep(
|
|
1173
|
+
computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers),
|
|
1174
|
+
headersOptions.signal,
|
|
1175
|
+
);
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
throw toUpstreamHttpError(outcome.status);
|
|
1179
|
+
}
|
|
490
1180
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
1181
|
+
const response = outcome;
|
|
1182
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
1183
|
+
throw toUpstreamHttpError(response.status);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (issued > 1) {
|
|
1187
|
+
const summary = telemetry.toTenantRetryPayload();
|
|
1188
|
+
if (summary) telemetry.observe(() => clientOptions.onRetrySummary?.(summary));
|
|
1189
|
+
}
|
|
1190
|
+
return response;
|
|
1191
|
+
} catch (error) {
|
|
1192
|
+
throwIfAmbientAborted(headersOptions.signal);
|
|
1193
|
+
if (!issuedThisAttempt) issued += 1;
|
|
1194
|
+
lastError = error;
|
|
1195
|
+
const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
|
|
1196
|
+
if (
|
|
1197
|
+
attempt < transportAttemptCap &&
|
|
1198
|
+
shouldRetryProxyTransportAttempt({
|
|
1199
|
+
error,
|
|
1200
|
+
explicitRetry,
|
|
1201
|
+
method: methodName,
|
|
1202
|
+
options: retryOptions,
|
|
1203
|
+
proxyUsed,
|
|
1204
|
+
})
|
|
1205
|
+
) {
|
|
1206
|
+
await sleep(
|
|
1207
|
+
computeProxyTransportRetryDelayMs(retryOptions, attempt),
|
|
1208
|
+
headersOptions.signal,
|
|
1209
|
+
);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
throw error;
|
|
519
1213
|
}
|
|
520
|
-
throw error;
|
|
521
1214
|
}
|
|
522
|
-
}
|
|
523
1215
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
1216
|
+
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
1217
|
+
// e.g. the final offsets of a partial allocation all resolved to
|
|
1218
|
+
// already-tried endpoints and were skipped. Surface the last real transport
|
|
1219
|
+
// failure rather than a synthetic exhaustion error.
|
|
1220
|
+
if (lastError !== undefined) {
|
|
1221
|
+
throw lastError;
|
|
1222
|
+
}
|
|
1223
|
+
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
1224
|
+
code: "retry_exhausted",
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
527
1227
|
}
|
|
528
1228
|
|
|
529
1229
|
async function streamRequest(
|
|
@@ -531,16 +1231,56 @@ export function createHttpClient(
|
|
|
531
1231
|
method: string,
|
|
532
1232
|
options: RequestOptions & { body?: unknown } = {},
|
|
533
1233
|
): Promise<HttpStreamResponse> {
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1234
|
+
const { headersOptions, methodName } = (() => {
|
|
1235
|
+
try {
|
|
1236
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
1237
|
+
throw new TransportError(
|
|
1238
|
+
"ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared",
|
|
1239
|
+
{ code: "transport_invalid_url" },
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
assertNoHttpTransportOverrides(options);
|
|
1243
|
+
const redirectPolicy = snapshotHttpRedirectPolicy(options);
|
|
1244
|
+
return {
|
|
1245
|
+
headersOptions: {
|
|
1246
|
+
...withClientHeaders(options, clientOptions, options.body),
|
|
1247
|
+
redirectPolicy,
|
|
1248
|
+
},
|
|
1249
|
+
methodName: normalizeHttpMethod(method),
|
|
1250
|
+
};
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
1253
|
+
}
|
|
1254
|
+
})();
|
|
1255
|
+
// Streams never retry. Observe the header fetch without consuming its body.
|
|
1256
|
+
const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), "off");
|
|
1257
|
+
const started = performance.now();
|
|
1258
|
+
const alreadyAborted = headersOptions.signal?.aborted === true;
|
|
1259
|
+
let proxyUsed = false;
|
|
1260
|
+
const record = (event: Omit<HttpAttemptTelemetryEvent, "ms" | "proxyUsed">) => {
|
|
1261
|
+
if (alreadyAborted) return;
|
|
1262
|
+
telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
|
|
1263
|
+
};
|
|
1264
|
+
try {
|
|
1265
|
+
const response = await fetchNativeHttpStream(
|
|
1266
|
+
baseUrl,
|
|
1267
|
+
url,
|
|
1268
|
+
methodName,
|
|
1269
|
+
headersOptions,
|
|
1270
|
+
clientOptions,
|
|
1271
|
+
warnOnce,
|
|
1272
|
+
(used) => {
|
|
1273
|
+
proxyUsed = used;
|
|
1274
|
+
},
|
|
538
1275
|
);
|
|
1276
|
+
record({ status: response.status });
|
|
1277
|
+
return response;
|
|
1278
|
+
} catch (error) {
|
|
1279
|
+
record(httpAttemptFailure(error));
|
|
1280
|
+
throw error;
|
|
1281
|
+
} finally {
|
|
1282
|
+
telemetry.finish(performance.now() - started);
|
|
539
1283
|
}
|
|
540
|
-
assertNoHttpTransportOverrides(options);
|
|
541
|
-
const headersOptions = withClientHeaders(options, clientOptions, options.body);
|
|
542
|
-
const methodName = normalizeHttpMethod(method);
|
|
543
|
-
return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
|
|
544
1284
|
}
|
|
545
1285
|
|
|
546
1286
|
return {
|
|
@@ -556,10 +1296,16 @@ export function createHttpClient(
|
|
|
556
1296
|
url,
|
|
557
1297
|
options: RequestWithMethodOptions = {},
|
|
558
1298
|
): Promise<AsyncIterable<SseMessage>> => {
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
1299
|
+
const callerHeaders = options.headers;
|
|
1300
|
+
const headers: Record<string, string> | HttpHeadersFactory =
|
|
1301
|
+
typeof callerHeaders === "function"
|
|
1302
|
+
? async (attempt) => {
|
|
1303
|
+
// Pass a malformed result through unchanged so the factory
|
|
1304
|
+
// validation rejects it instead of spreading it into `{}`.
|
|
1305
|
+
const minted = await callerHeaders(attempt);
|
|
1306
|
+
return isHeaderRecord(minted) ? { Accept: "text/event-stream", ...minted } : minted;
|
|
1307
|
+
}
|
|
1308
|
+
: { Accept: "text/event-stream", ...callerHeaders };
|
|
563
1309
|
const response = await streamRequest(url, options.method ?? "GET", {
|
|
564
1310
|
...options,
|
|
565
1311
|
headers,
|