@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.51
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 +537 -33
- package/CHANGELOG.md +205 -1
- package/README.md +83 -22
- package/SUBMISSION.md +2 -2
- package/bin/apifuse-check.ts +165 -11
- package/bin/apifuse-dev.ts +57 -13
- package/bin/apifuse-migrate-operation-declaration.ts +55 -0
- package/bin/apifuse-migrate-shape.ts +202 -0
- package/bin/apifuse-pack-check.ts +22 -2
- package/bin/apifuse-pack-smoke.ts +57 -2
- package/bin/apifuse-pack-types.ts +357 -38
- package/bin/apifuse-perf.ts +14 -13
- package/bin/apifuse-record.ts +709 -72
- package/bin/apifuse-submit-check.ts +2344 -324
- package/bin/apifuse-sync-assets.ts +117 -0
- 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 +27 -0
- package/dist/cli/create.d.ts +4 -1
- package/dist/cli/create.js +42 -37
- package/dist/cli/migrate-operation-declaration.d.ts +59 -0
- package/dist/cli/migrate-operation-declaration.js +1249 -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 +52 -0
- package/dist/cli/migrate-provider-shape.js +578 -0
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
- package/dist/cli/templates/provider/README.md.tpl +5 -5
- 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 +179 -16
- package/dist/config/loader.js +435 -127
- 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 +33 -0
- package/dist/declaration-validation.js +266 -0
- package/dist/define.d.ts +66 -29
- package/dist/define.js +778 -69
- package/dist/dev.d.ts +3 -0
- package/dist/dev.js +1 -1
- package/dist/engine.d.ts +78 -0
- package/dist/engine.js +133 -0
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/error-resolution.d.ts +4 -0
- package/dist/error-resolution.js +122 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +40 -0
- package/dist/fixture-sanitization.d.ts +28 -0
- package/dist/fixture-sanitization.js +227 -0
- package/dist/health-scenario.d.ts +1842 -0
- package/dist/health-scenario.js +624 -0
- package/dist/index.d.ts +21 -10
- package/dist/index.js +12 -7
- package/dist/lint.d.ts +23 -11
- package/dist/lint.js +572 -69
- 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 +10 -2
- package/dist/provider.js +4 -1
- package/dist/runtime/auth-flow.d.ts +5 -1
- package/dist/runtime/auth-flow.js +6 -0
- package/dist/runtime/browser.d.ts +1 -0
- package/dist/runtime/browser.js +492 -49
- package/dist/runtime/cache.d.ts +1 -0
- package/dist/runtime/cache.js +169 -15
- package/dist/runtime/choice-wordlist.d.ts +9 -0
- package/dist/runtime/choice-wordlist.js +138 -0
- package/dist/runtime/choice.d.ts +13 -1
- package/dist/runtime/choice.js +490 -102
- package/dist/runtime/chrome149-header-order.d.ts +58 -0
- package/dist/runtime/chrome149-header-order.js +289 -0
- package/dist/runtime/env.js +12 -0
- package/dist/runtime/executor.d.ts +3 -2
- package/dist/runtime/executor.js +26 -25
- package/dist/runtime/http.d.ts +1 -0
- package/dist/runtime/http.js +515 -53
- package/dist/runtime/insights.js +2 -2
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +366 -8
- 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 +96 -0
- package/dist/runtime/native-network.js +1232 -0
- package/dist/runtime/ocr.d.ts +29 -0
- package/dist/runtime/ocr.js +440 -0
- package/dist/runtime/otlp.d.ts +71 -2
- package/dist/runtime/otlp.js +397 -16
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-telemetry.d.ts +80 -1
- package/dist/runtime/proxy-telemetry.js +154 -47
- package/dist/runtime/redirects.d.ts +29 -0
- package/dist/runtime/redirects.js +36 -0
- package/dist/runtime/redis.d.ts +1 -1
- package/dist/runtime/redis.js +4 -2
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/resolver-config.d.ts +6 -0
- package/dist/runtime/resolver-config.js +6 -0
- package/dist/runtime/resolver-public.d.ts +1 -0
- package/dist/runtime/resolver-public.js +1 -0
- package/dist/runtime/resolver-shared.d.ts +14 -0
- package/dist/runtime/resolver-shared.js +12 -0
- package/dist/runtime/resolver-vendors/bindings.d.ts +48 -0
- package/dist/runtime/resolver-vendors/bindings.js +40 -0
- package/dist/runtime/resolver-vendors/browser.d.ts +22 -0
- package/dist/runtime/resolver-vendors/browser.js +377 -0
- package/dist/runtime/resolver-vendors/capsolver.d.ts +22 -0
- package/dist/runtime/resolver-vendors/capsolver.js +531 -0
- package/dist/runtime/resolver-vendors/hosts.d.ts +2 -0
- package/dist/runtime/resolver-vendors/hosts.js +33 -0
- package/dist/runtime/resolver-vendors/twocaptcha.d.ts +24 -0
- package/dist/runtime/resolver-vendors/twocaptcha.js +408 -0
- package/dist/runtime/resolver-vendors/types.d.ts +94 -0
- package/dist/runtime/resolver-vendors/types.js +96 -0
- package/dist/runtime/resolver.d.ts +72 -0
- package/dist/runtime/resolver.js +771 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +3 -0
- package/dist/runtime/state.js +277 -71
- package/dist/runtime/stealth-cookies.d.ts +20 -0
- package/dist/runtime/stealth-cookies.js +111 -0
- package/dist/runtime/stealth.d.ts +41 -5
- package/dist/runtime/stealth.js +997 -291
- package/dist/runtime/stt.js +1 -12
- 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 +5 -0
- package/dist/runtime/trace.js +43 -10
- 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/index.d.ts +5 -2
- package/dist/server/index.js +2 -2
- 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.d.ts +14 -3
- package/dist/server/self-test.js +129 -58
- package/dist/server/serve-implementation.d.ts +230 -0
- package/dist/server/serve-implementation.js +2265 -0
- package/dist/server/serve.d.ts +1 -70
- package/dist/server/serve.js +1 -1131
- package/dist/server/trace-output.d.ts +6 -0
- package/dist/server/trace-output.js +79 -0
- package/dist/server/types.d.ts +30 -5
- package/dist/server/types.js +13 -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 +215 -0
- package/dist/stateful/stateful-provider-runtime-context.d.ts +32 -0
- package/dist/stateful/stateful-provider-runtime-context.js +60 -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 +9 -4
- package/dist/stealth/profiles.js +160 -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 +489 -21
- package/dist/trace-sanitization.d.ts +5 -0
- package/dist/trace-sanitization.js +45 -0
- package/dist/types.d.ts +673 -131
- package/dist/types.js +1 -0
- package/package.json +45 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +118 -0
- package/src/ceremonies/index.ts +189 -46
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/approval-override.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/codemod-syntax.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/connection-precedence.ts.txt +10 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/docs-conflict.ts.txt +8 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-hoisted-array-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-imported-array-spread.ts.txt +9 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-map.ts.txt +5 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-mixed-array-spread.ts.txt +24 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/examples-operation.ts.txt +16 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/factory-map.ts.txt +3 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoist-all.ts.txt +31 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/hoisted-const.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/imported-spread.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-map.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-cast-tail.ts.txt +21 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-ekitan.ts.txt +11 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/inline-spread-override.ts.txt +14 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/missing-english-locale.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/no-safety.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/non-literal.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/redundant-approval.ts.txt +6 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/safety-conflict.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/stream.ts.txt +7 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/tool-router-spread.ts.txt +15 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/unparseable.ts.txt +4 -0
- package/src/cli/__tests__/fixtures/migrate-operation-declaration/verbatim-template.ts.txt +12 -0
- package/src/cli/commands.ts +33 -0
- package/src/cli/create.ts +50 -37
- package/src/cli/migrate-operation-declaration.ts +1751 -0
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +772 -0
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/cli/templates/provider/Dockerfile.tpl +1 -1
- package/src/cli/templates/provider/README.md.tpl +5 -5
- 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 +690 -163
- 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 +330 -0
- package/src/define.ts +1048 -131
- package/src/dev.ts +4 -1
- package/src/engine.ts +279 -0
- package/src/error-observability.ts +64 -0
- package/src/error-resolution.ts +127 -0
- package/src/errors.ts +68 -0
- package/src/fixture-sanitization.ts +264 -0
- package/src/health-scenario.ts +875 -0
- package/src/index.ts +220 -13
- package/src/lint.ts +698 -90
- 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 +161 -3
- package/src/runtime/auth-flow.ts +12 -0
- package/src/runtime/browser.ts +661 -63
- package/src/runtime/cache.ts +189 -14
- package/src/runtime/choice-wordlist.ts +145 -0
- package/src/runtime/choice.ts +631 -120
- package/src/runtime/chrome149-header-order.ts +330 -0
- package/src/runtime/env.ts +13 -0
- package/src/runtime/executor.ts +43 -31
- package/src/runtime/http.ts +641 -61
- package/src/runtime/insights.ts +2 -2
- package/src/runtime/instrumentation.ts +520 -15
- package/src/runtime/native-network-errors.ts +99 -0
- package/src/runtime/native-network.ts +1605 -0
- package/src/runtime/ocr.ts +523 -0
- package/src/runtime/otlp.ts +467 -21
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-telemetry.ts +244 -75
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +7 -2
- package/src/runtime/request-options.ts +680 -1
- package/src/runtime/resolver-config.ts +6 -0
- package/src/runtime/resolver-public.ts +23 -0
- package/src/runtime/resolver-shared.ts +33 -0
- package/src/runtime/resolver-vendors/bindings.ts +56 -0
- package/src/runtime/resolver-vendors/browser.ts +533 -0
- package/src/runtime/resolver-vendors/capsolver.ts +707 -0
- package/src/runtime/resolver-vendors/hosts.ts +38 -0
- package/src/runtime/resolver-vendors/twocaptcha.ts +540 -0
- package/src/runtime/resolver-vendors/types.ts +212 -0
- package/src/runtime/resolver.ts +1157 -0
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +394 -77
- package/src/runtime/stealth-cookies.ts +132 -0
- package/src/runtime/stealth.ts +1280 -336
- package/src/runtime/stt.ts +1 -19
- package/src/runtime/timeout.ts +18 -0
- package/src/runtime/trace-config.ts +78 -0
- package/src/runtime/trace.ts +57 -17
- package/src/serve.ts +6 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +34 -2
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test.ts +191 -68
- package/src/server/serve-implementation.ts +3378 -0
- package/src/server/serve.ts +1 -1632
- package/src/server/trace-output.ts +129 -0
- package/src/server/types.ts +13 -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 +287 -0
- package/src/stateful/stateful-provider-runtime-context.ts +92 -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 +202 -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 +658 -15
- package/src/trace-sanitization.ts +63 -0
- package/src/types.ts +788 -174
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
package/dist/runtime/http.js
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
|
-
import { resolveProxyConfigAsync } from "../config/loader.js";
|
|
2
|
-
import { ProviderError, TransportError } from "../errors.js";
|
|
1
|
+
import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, } from "../config/loader.js";
|
|
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 {
|
|
5
|
+
import { evaluateRedirectHop, isRedirectStatus, resolveRedirectUrl } from "./redirects.js";
|
|
6
|
+
import { normalizeHttpRequestBody, redactSensitiveError, redactSensitiveRequestError, serializeRequestUrl, } from "./request-options.js";
|
|
6
7
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
7
8
|
function isHttpStatusOutcome(outcome) {
|
|
8
9
|
return "kind" in outcome && outcome.kind === "http-status";
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
function isDedupeSkipOutcome(outcome) {
|
|
12
|
+
return "kind" in outcome && outcome.kind === "dedupe-skip";
|
|
13
|
+
}
|
|
14
|
+
function toAmbientCancellationError(signal, error = signal.reason) {
|
|
15
|
+
if (error instanceof TransportError && error.code === "transport_cancelled") {
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
18
|
+
return new TransportError("Request cancelled", {
|
|
19
|
+
code: "transport_cancelled",
|
|
20
|
+
status: 0,
|
|
21
|
+
retryable: false,
|
|
22
|
+
...(error !== undefined
|
|
23
|
+
? { cause: error instanceof Error ? error : new Error(String(error)) }
|
|
24
|
+
: {}),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function throwIfAmbientAborted(signal) {
|
|
28
|
+
if (signal?.aborted)
|
|
29
|
+
throw toAmbientCancellationError(signal);
|
|
30
|
+
}
|
|
31
|
+
async function sleep(ms, signal) {
|
|
32
|
+
throwIfAmbientAborted(signal);
|
|
11
33
|
if (ms <= 0)
|
|
12
34
|
return;
|
|
13
|
-
|
|
35
|
+
if (!signal) {
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
await new Promise((resolve, reject) => {
|
|
40
|
+
const onAbort = () => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
reject(toAmbientCancellationError(signal));
|
|
43
|
+
};
|
|
44
|
+
const timer = setTimeout(() => {
|
|
45
|
+
signal.removeEventListener("abort", onAbort);
|
|
46
|
+
resolve();
|
|
47
|
+
}, ms);
|
|
48
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
49
|
+
});
|
|
14
50
|
}
|
|
15
51
|
function toUpstreamHttpError(status) {
|
|
16
52
|
return new TransportError(`Upstream request failed with status ${status}`, {
|
|
@@ -49,7 +85,17 @@ function parseJson(body) {
|
|
|
49
85
|
function isTimeoutMessage(message) {
|
|
50
86
|
return /\b(timed out|timeout|deadline exceeded)\b/i.test(message);
|
|
51
87
|
}
|
|
52
|
-
function toHttpTransportError(error) {
|
|
88
|
+
function toHttpTransportError(error, ambientSignal, timeoutSignal) {
|
|
89
|
+
if (ambientSignal?.aborted) {
|
|
90
|
+
return toAmbientCancellationError(ambientSignal, error);
|
|
91
|
+
}
|
|
92
|
+
if (timeoutSignal?.aborted) {
|
|
93
|
+
return new TransportError("Request timed out", {
|
|
94
|
+
code: "transport_timeout",
|
|
95
|
+
status: 0,
|
|
96
|
+
...(error instanceof Error ? { cause: error } : {}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
53
99
|
if (error instanceof TransportError) {
|
|
54
100
|
if (error.code) {
|
|
55
101
|
return error;
|
|
@@ -121,9 +167,108 @@ function requireNativeResponseBody(response) {
|
|
|
121
167
|
}
|
|
122
168
|
return response.body;
|
|
123
169
|
}
|
|
124
|
-
function
|
|
170
|
+
function mergeAbortSignals(...signals) {
|
|
171
|
+
const activeSignals = signals.filter((signal) => signal != null);
|
|
172
|
+
if (activeSignals.length === 0)
|
|
173
|
+
return undefined;
|
|
174
|
+
if (activeSignals.length === 1)
|
|
175
|
+
return activeSignals[0];
|
|
176
|
+
return AbortSignal.any(activeSignals);
|
|
177
|
+
}
|
|
178
|
+
function cancelStreamOnAbort(body, signal) {
|
|
179
|
+
if (!signal)
|
|
180
|
+
return body;
|
|
181
|
+
let reader;
|
|
182
|
+
let finished = false;
|
|
183
|
+
let streamController;
|
|
184
|
+
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
185
|
+
const onAbort = () => {
|
|
186
|
+
if (finished)
|
|
187
|
+
return;
|
|
188
|
+
finished = true;
|
|
189
|
+
cleanup();
|
|
190
|
+
const reason = toAmbientCancellationError(signal);
|
|
191
|
+
streamController?.error(reason);
|
|
192
|
+
const cancellation = reader ? reader.cancel(reason) : body.cancel(reason);
|
|
193
|
+
void cancellation.catch(() => undefined).finally(() => reader?.releaseLock());
|
|
194
|
+
};
|
|
195
|
+
return new ReadableStream({
|
|
196
|
+
start(controller) {
|
|
197
|
+
streamController = controller;
|
|
198
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
199
|
+
if (signal.aborted)
|
|
200
|
+
onAbort();
|
|
201
|
+
},
|
|
202
|
+
async pull(controller) {
|
|
203
|
+
try {
|
|
204
|
+
reader ??= body.getReader();
|
|
205
|
+
const chunk = await reader.read();
|
|
206
|
+
if (finished)
|
|
207
|
+
return;
|
|
208
|
+
if (chunk.done) {
|
|
209
|
+
finished = true;
|
|
210
|
+
cleanup();
|
|
211
|
+
controller.close();
|
|
212
|
+
reader.releaseLock();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
controller.enqueue(chunk.value);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
if (finished)
|
|
219
|
+
return;
|
|
220
|
+
finished = true;
|
|
221
|
+
cleanup();
|
|
222
|
+
controller.error(error);
|
|
223
|
+
reader?.releaseLock();
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
async cancel(reason) {
|
|
227
|
+
if (finished)
|
|
228
|
+
return;
|
|
229
|
+
finished = true;
|
|
230
|
+
cleanup();
|
|
231
|
+
try {
|
|
232
|
+
await (reader ? reader.cancel(reason) : body.cancel(reason));
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
reader?.releaseLock();
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
}, { highWaterMark: 0 });
|
|
239
|
+
}
|
|
240
|
+
function sanitizeStreamErrors(body, serializedUrl) {
|
|
241
|
+
if (serializedUrl.sensitiveValues.length === 0)
|
|
242
|
+
return body;
|
|
243
|
+
let reader;
|
|
244
|
+
return new ReadableStream({
|
|
245
|
+
async pull(controller) {
|
|
246
|
+
try {
|
|
247
|
+
reader ??= body.getReader();
|
|
248
|
+
const chunk = await reader.read();
|
|
249
|
+
if (chunk.done) {
|
|
250
|
+
controller.close();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
controller.enqueue(chunk.value);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
controller.error(redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl));
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
async cancel(reason) {
|
|
260
|
+
try {
|
|
261
|
+
await (reader ? reader.cancel(reason) : body.cancel(reason));
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
}, { highWaterMark: 0 });
|
|
268
|
+
}
|
|
269
|
+
function toNativeHttpStreamResponse(response, serializedUrl, signal) {
|
|
125
270
|
const headers = Object.fromEntries(response.headers.entries());
|
|
126
|
-
const body = requireNativeResponseBody(response);
|
|
271
|
+
const body = sanitizeStreamErrors(cancelStreamOnAbort(requireNativeResponseBody(response), signal), serializedUrl);
|
|
127
272
|
return {
|
|
128
273
|
body,
|
|
129
274
|
headers,
|
|
@@ -164,17 +309,194 @@ function isAbsoluteUrl(url) {
|
|
|
164
309
|
function resolveHttpUrl(baseUrl, url) {
|
|
165
310
|
return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
|
|
166
311
|
}
|
|
312
|
+
const MAX_HTTP_REDIRECT_HOPS = 20;
|
|
313
|
+
const HTTP_REDIRECT_POLICY_FIELDS = new Set(["mode", "maxHops"]);
|
|
314
|
+
const REDIRECT_BODY_HEADERS = new Set([
|
|
315
|
+
"content-encoding",
|
|
316
|
+
"content-language",
|
|
317
|
+
"content-location",
|
|
318
|
+
"content-type",
|
|
319
|
+
]);
|
|
320
|
+
const MALFORMED_REDIRECT_TARGET = "[malformed redirect target]";
|
|
321
|
+
function invalidHttpRedirectPolicy(message, cause) {
|
|
322
|
+
return new TransportError(`Invalid ctx.http redirectPolicy: ${message}`, {
|
|
323
|
+
code: "http_redirect_policy_invalid",
|
|
324
|
+
...(cause ? { cause } : {}),
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/** Snapshot untrusted caller input synchronously, before proxy resolution or fetch. */
|
|
328
|
+
function normalizeHttpRedirectPolicy(value) {
|
|
329
|
+
if (value === undefined)
|
|
330
|
+
return undefined;
|
|
331
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
332
|
+
throw invalidHttpRedirectPolicy("expected an object");
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const keys = Reflect.ownKeys(value);
|
|
336
|
+
for (const key of keys) {
|
|
337
|
+
if (typeof key !== "string" || !HTTP_REDIRECT_POLICY_FIELDS.has(key)) {
|
|
338
|
+
throw invalidHttpRedirectPolicy(`unknown field ${String(key)}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
for (const field of HTTP_REDIRECT_POLICY_FIELDS) {
|
|
342
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, field);
|
|
343
|
+
if (!descriptor || !("value" in descriptor)) {
|
|
344
|
+
throw invalidHttpRedirectPolicy(`${field} must be an own data property`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const record = value;
|
|
348
|
+
if (record.mode !== "same-origin") {
|
|
349
|
+
throw invalidHttpRedirectPolicy('mode must be "same-origin"');
|
|
350
|
+
}
|
|
351
|
+
if (typeof record.maxHops !== "number" ||
|
|
352
|
+
!Number.isInteger(record.maxHops) ||
|
|
353
|
+
record.maxHops < 0 ||
|
|
354
|
+
record.maxHops > MAX_HTTP_REDIRECT_HOPS) {
|
|
355
|
+
throw invalidHttpRedirectPolicy(`maxHops must be an integer from 0 to ${MAX_HTTP_REDIRECT_HOPS}`);
|
|
356
|
+
}
|
|
357
|
+
return { mode: "same-origin", maxHops: record.maxHops };
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
if (error instanceof TransportError)
|
|
361
|
+
throw error;
|
|
362
|
+
throw invalidHttpRedirectPolicy("could not be inspected safely", error instanceof Error ? error : undefined);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function snapshotHttpRedirectPolicy(options) {
|
|
366
|
+
try {
|
|
367
|
+
return normalizeHttpRedirectPolicy(options.redirectPolicy);
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
if (error instanceof TransportError)
|
|
371
|
+
throw error;
|
|
372
|
+
throw invalidHttpRedirectPolicy("could not be read safely", error instanceof Error ? error : undefined);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function withoutRedirectBodyHeaders(headers) {
|
|
376
|
+
const nextHeaders = new Headers(headers);
|
|
377
|
+
for (const name of REDIRECT_BODY_HEADERS)
|
|
378
|
+
nextHeaders.delete(name);
|
|
379
|
+
return nextHeaders;
|
|
380
|
+
}
|
|
381
|
+
function redirectDiagnosticTarget(value) {
|
|
382
|
+
try {
|
|
383
|
+
const parsed = new URL(value);
|
|
384
|
+
// Origin omits URL userinfo. Keeping only origin + path makes diagnostics
|
|
385
|
+
// useful while structurally excluding every query value and fragment,
|
|
386
|
+
// including attacker-chosen keys the provider did not declare sensitive.
|
|
387
|
+
const redactedQuery = parsed.search ? "?[REDACTED]" : "";
|
|
388
|
+
return parsed.origin === "null"
|
|
389
|
+
? `${parsed.protocol}<opaque-target>`
|
|
390
|
+
: `${parsed.origin}${parsed.pathname}${redactedQuery}`;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
return MALFORMED_REDIRECT_TARGET;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function discardRedirectResponseBody(response) {
|
|
397
|
+
try {
|
|
398
|
+
const cancellation = response.body?.cancel();
|
|
399
|
+
if (cancellation)
|
|
400
|
+
void cancellation.catch(() => undefined);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
// The redirect decision is security-significant and must not be replaced
|
|
404
|
+
// or delayed by an upstream body's cancellation failure. Cancellation was
|
|
405
|
+
// attempted; redirect evaluation continues without awaiting its completion.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function fetchWithHttpRedirectPolicy(requestUrl, requestInit, policy) {
|
|
409
|
+
if (!policy)
|
|
410
|
+
return fetch(requestUrl, requestInit);
|
|
411
|
+
const initialUrl = new URL(requestUrl);
|
|
412
|
+
if (initialUrl.protocol !== "http:" && initialUrl.protocol !== "https:") {
|
|
413
|
+
throw new TransportError("ctx.http redirectPolicy requires an HTTP(S) origin", {
|
|
414
|
+
code: "transport_invalid_url",
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
const initialOrigin = initialUrl.origin;
|
|
418
|
+
let currentUrl = requestUrl;
|
|
419
|
+
let method = normalizeHttpMethod(requestInit.method ?? "GET");
|
|
420
|
+
let body = requestInit.body;
|
|
421
|
+
let headers = requestInit.headers;
|
|
422
|
+
let followedHops = 0;
|
|
423
|
+
const visitedRequests = new Set([`${method} ${currentUrl}`]);
|
|
424
|
+
while (true) {
|
|
425
|
+
const response = await fetch(currentUrl, {
|
|
426
|
+
...requestInit,
|
|
427
|
+
body,
|
|
428
|
+
headers,
|
|
429
|
+
method,
|
|
430
|
+
redirect: "manual",
|
|
431
|
+
});
|
|
432
|
+
if (!isRedirectStatus(response.status))
|
|
433
|
+
return response;
|
|
434
|
+
const location = response.headers.get("location");
|
|
435
|
+
discardRedirectResponseBody(response);
|
|
436
|
+
let nextUrlString;
|
|
437
|
+
try {
|
|
438
|
+
nextUrlString = resolveRedirectUrl(location || undefined, currentUrl);
|
|
439
|
+
}
|
|
440
|
+
catch {
|
|
441
|
+
const target = MALFORMED_REDIRECT_TARGET;
|
|
442
|
+
throw new HttpRedirectError(`Redirect response has malformed Location target ${target}`, {
|
|
443
|
+
reason: "missing_location",
|
|
444
|
+
target,
|
|
445
|
+
status: response.status,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const decision = evaluateRedirectHop({
|
|
449
|
+
status: response.status,
|
|
450
|
+
method,
|
|
451
|
+
nextUrl: nextUrlString,
|
|
452
|
+
shouldStop: nextUrlString ? new URL(nextUrlString).origin !== initialOrigin : false,
|
|
453
|
+
redirectCount: followedHops + 1,
|
|
454
|
+
maxHops: policy.maxHops,
|
|
455
|
+
visitedRequests,
|
|
456
|
+
});
|
|
457
|
+
if (decision.kind === "stop") {
|
|
458
|
+
const target = decision.nextUrl ? redirectDiagnosticTarget(decision.nextUrl) : undefined;
|
|
459
|
+
const message = (() => {
|
|
460
|
+
switch (decision.reason) {
|
|
461
|
+
case "stopped":
|
|
462
|
+
return `Redirect policy refused cross-origin target ${target}`;
|
|
463
|
+
case "max_hops":
|
|
464
|
+
return `Redirect policy reached maxHops before target ${target}`;
|
|
465
|
+
case "loop":
|
|
466
|
+
return `Redirect loop refused target ${target}`;
|
|
467
|
+
case "missing_location":
|
|
468
|
+
return `Redirect response from ${redirectDiagnosticTarget(currentUrl)} is missing Location`;
|
|
469
|
+
}
|
|
470
|
+
})();
|
|
471
|
+
throw new HttpRedirectError(message, {
|
|
472
|
+
reason: decision.reason,
|
|
473
|
+
...(target ? { target } : {}),
|
|
474
|
+
status: response.status,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (decision.nextMethod !== method) {
|
|
478
|
+
body = undefined;
|
|
479
|
+
headers = withoutRedirectBodyHeaders(headers);
|
|
480
|
+
}
|
|
481
|
+
method = decision.nextMethod;
|
|
482
|
+
currentUrl = decision.nextUrl;
|
|
483
|
+
followedHops += 1;
|
|
484
|
+
visitedRequests.add(`${method} ${currentUrl}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
167
487
|
async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
|
|
168
488
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
169
489
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
170
490
|
upstream: clientOptions.upstream,
|
|
171
|
-
apifuseConfig: clientOptions.apifuseConfig,
|
|
172
491
|
proxyPolicy: clientOptions.proxyPolicy,
|
|
173
492
|
affinityKey: clientOptions.affinityKey,
|
|
174
493
|
proxyAttempt: computeProxyAttemptIndex({
|
|
175
494
|
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
176
495
|
retryAttemptOffset: proxyAttemptOffset,
|
|
177
496
|
}),
|
|
497
|
+
// Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
|
|
498
|
+
// supported here, so a socks5 policy fails loudly rather than downgrading.
|
|
499
|
+
transportProtocols: ["http"],
|
|
178
500
|
telemetry: clientOptions.telemetry,
|
|
179
501
|
});
|
|
180
502
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -196,27 +518,50 @@ function normalizeNativeFetchBody(body) {
|
|
|
196
518
|
copied.set(normalized);
|
|
197
519
|
return copied.buffer;
|
|
198
520
|
}
|
|
199
|
-
|
|
200
|
-
|
|
521
|
+
function serializeHttpRequestUrl(baseUrl, url, options) {
|
|
522
|
+
try {
|
|
523
|
+
return serializeRequestUrl(resolveHttpUrl(baseUrl, url), options.params, options.sensitiveParams);
|
|
524
|
+
}
|
|
525
|
+
catch (error) {
|
|
526
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
|
|
530
|
+
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
531
|
+
const { requestUrl } = serializedUrl;
|
|
201
532
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
533
|
+
const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
|
|
202
534
|
const timeoutHandle = options.timeout
|
|
203
535
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
204
536
|
: undefined;
|
|
205
537
|
let proxy;
|
|
206
538
|
try {
|
|
539
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
540
|
+
// Resolve inside the try (and after the timeout is armed) so allocator
|
|
541
|
+
// failures are branded as TransportErrors and count against the request
|
|
542
|
+
// deadline, exactly as an inline resolve would.
|
|
207
543
|
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
544
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
545
|
+
// For a registry allocator chain, skip an endpoint a prior attempt already
|
|
546
|
+
// tried rather than re-issuing the same request. Returning the sentinel
|
|
547
|
+
// (instead of breaking) lets the loop keep advancing the flat offset until
|
|
548
|
+
// it crosses into the fallback vendor's pool span.
|
|
549
|
+
if (dedupe && proxy) {
|
|
550
|
+
if (dedupe.attempted.has(proxy)) {
|
|
551
|
+
return { kind: "dedupe-skip" };
|
|
552
|
+
}
|
|
553
|
+
dedupe.attempted.add(proxy);
|
|
554
|
+
}
|
|
208
555
|
const requestInit = {
|
|
209
556
|
headers: options.headers,
|
|
210
557
|
method,
|
|
211
558
|
...(proxy ? { proxy } : {}),
|
|
212
|
-
signal:
|
|
559
|
+
...(signal ? { signal } : {}),
|
|
213
560
|
};
|
|
214
561
|
if (options.body !== undefined) {
|
|
215
562
|
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
216
563
|
}
|
|
217
|
-
const response = await
|
|
218
|
-
...requestInit,
|
|
219
|
-
});
|
|
564
|
+
const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
|
|
220
565
|
const headers = Object.fromEntries(response.headers.entries());
|
|
221
566
|
if (statusRetryCodes && response.status >= 400) {
|
|
222
567
|
await drainNativeResponseBody(response);
|
|
@@ -239,9 +584,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
239
584
|
}
|
|
240
585
|
catch (error) {
|
|
241
586
|
if (error instanceof SyntaxError) {
|
|
242
|
-
throw error;
|
|
587
|
+
throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
243
588
|
}
|
|
244
|
-
const transportError = toHttpTransportError(error);
|
|
589
|
+
const transportError = redactSensitiveError(toHttpTransportError(error, clientOptions.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
245
590
|
transportError.proxyUsed = Boolean(proxy);
|
|
246
591
|
throw transportError;
|
|
247
592
|
}
|
|
@@ -251,25 +596,27 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
251
596
|
}
|
|
252
597
|
}
|
|
253
598
|
async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn) {
|
|
254
|
-
const
|
|
599
|
+
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
600
|
+
const { requestUrl } = serializedUrl;
|
|
255
601
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
602
|
+
const signal = mergeAbortSignals(clientOptions.signal, controller?.signal);
|
|
256
603
|
const timeoutHandle = options.timeout
|
|
257
604
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
258
605
|
: undefined;
|
|
259
606
|
try {
|
|
607
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
260
608
|
const proxy = await resolveNativeProxy(options, clientOptions, warn);
|
|
609
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
261
610
|
const requestInit = {
|
|
262
611
|
headers: options.headers,
|
|
263
612
|
method,
|
|
264
613
|
...(proxy ? { proxy } : {}),
|
|
265
|
-
signal:
|
|
614
|
+
...(signal ? { signal } : {}),
|
|
266
615
|
};
|
|
267
616
|
if (options.body !== undefined) {
|
|
268
617
|
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
269
618
|
}
|
|
270
|
-
const response = await
|
|
271
|
-
...requestInit,
|
|
272
|
-
});
|
|
619
|
+
const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
|
|
273
620
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
274
621
|
await drainNativeResponseBody(response);
|
|
275
622
|
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
@@ -277,13 +624,15 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
|
|
|
277
624
|
status: response.status,
|
|
278
625
|
});
|
|
279
626
|
}
|
|
280
|
-
|
|
627
|
+
// Per-call timeout remains header-scoped, while the ambient request signal
|
|
628
|
+
// stays attached to the response body for its full consumption lifetime.
|
|
629
|
+
return toNativeHttpStreamResponse(response, serializedUrl, clientOptions.signal);
|
|
281
630
|
}
|
|
282
631
|
catch (error) {
|
|
283
632
|
if (error instanceof SyntaxError) {
|
|
284
|
-
throw error;
|
|
633
|
+
throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
285
634
|
}
|
|
286
|
-
throw toHttpTransportError(error);
|
|
635
|
+
throw redactSensitiveError(toHttpTransportError(error, clientOptions.signal, controller?.signal), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
287
636
|
}
|
|
288
637
|
finally {
|
|
289
638
|
if (timeoutHandle)
|
|
@@ -301,19 +650,31 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
301
650
|
warn(message);
|
|
302
651
|
};
|
|
303
652
|
async function request(url, method, options = {}) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
653
|
+
const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
|
|
654
|
+
try {
|
|
655
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
656
|
+
throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
|
|
657
|
+
}
|
|
658
|
+
assertNoHttpTransportOverrides(options);
|
|
659
|
+
const redirectPolicy = snapshotHttpRedirectPolicy(options);
|
|
660
|
+
const headersOptions = {
|
|
661
|
+
...withClientHeaders(options, clientOptions, options.body),
|
|
662
|
+
redirectPolicy,
|
|
663
|
+
};
|
|
664
|
+
const methodName = normalizeHttpMethod(method);
|
|
665
|
+
const explicitRetry = headersOptions.retry !== undefined;
|
|
666
|
+
const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
667
|
+
label: "HTTP",
|
|
668
|
+
}) ??
|
|
669
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
670
|
+
if (retryOptions)
|
|
671
|
+
validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
672
|
+
return { explicitRetry, headersOptions, methodName, retryOptions };
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
676
|
+
}
|
|
677
|
+
})();
|
|
317
678
|
const retryEnabled = Boolean(retryOptions &&
|
|
318
679
|
retryOptions.attempts > 1 &&
|
|
319
680
|
isProxyTransportRetryMethod(methodName, retryOptions));
|
|
@@ -325,23 +686,100 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
325
686
|
const attemptOptions = statusRetryEnabled
|
|
326
687
|
? { ...headersOptions, throwOnHttpError: false }
|
|
327
688
|
: headersOptions;
|
|
328
|
-
|
|
689
|
+
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
690
|
+
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
691
|
+
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
692
|
+
// spans), so a transport failure should advance to the next endpoint —
|
|
693
|
+
// potentially crossing into the fallback vendor — rather than stopping at
|
|
694
|
+
// the per-endpoint retry budget and stranding the request on the primary
|
|
695
|
+
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
696
|
+
// only for implicit, safe-method allocator requests; explicit retry
|
|
697
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
698
|
+
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
699
|
+
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
700
|
+
// the full span.
|
|
701
|
+
const policyProxy = (() => {
|
|
702
|
+
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
703
|
+
return policy && typeof policy === "object" ? policy : undefined;
|
|
704
|
+
})();
|
|
705
|
+
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
706
|
+
const transportAttemptCap = retryOptions
|
|
707
|
+
? resolvePolicyTransportAttemptCap({
|
|
708
|
+
policy: policyProxy,
|
|
709
|
+
usesPolicyAllocator,
|
|
710
|
+
retryAttempts: retryOptions.attempts,
|
|
711
|
+
explicitRetry,
|
|
712
|
+
method: methodName,
|
|
713
|
+
})
|
|
714
|
+
: 1;
|
|
715
|
+
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
716
|
+
// attempts rotate the flat offset across the concatenated vendor pool
|
|
717
|
+
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
718
|
+
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
719
|
+
// the offset reaches the next vendor. Rather than re-hammering an
|
|
720
|
+
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
721
|
+
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
722
|
+
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
723
|
+
// vendor's pool span instead of stalling on the primary vendor.
|
|
724
|
+
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
725
|
+
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
726
|
+
// an explicit retry policy: there the caller's `attempts` count is the
|
|
727
|
+
// contract and each attempt must issue against whatever endpoint it resolves
|
|
728
|
+
// — even a repeat — instead of being silently skipped (which would collapse
|
|
729
|
+
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
730
|
+
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
731
|
+
policy: policyProxy,
|
|
732
|
+
usesPolicyAllocator,
|
|
733
|
+
explicitRetry,
|
|
734
|
+
method: methodName,
|
|
735
|
+
});
|
|
736
|
+
const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
|
|
737
|
+
const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext);
|
|
329
738
|
if (!retryEnabled || !retryOptions) {
|
|
739
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
330
740
|
const outcome = await executeOnce();
|
|
741
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
742
|
+
// Single-shot path never de-duplicates (dedupeContext is undefined),
|
|
743
|
+
// but keep the union total.
|
|
744
|
+
throw new TransportError("HTTP request produced no terminal result", {
|
|
745
|
+
code: "retry_exhausted",
|
|
746
|
+
});
|
|
747
|
+
}
|
|
331
748
|
if (isHttpStatusOutcome(outcome)) {
|
|
332
749
|
throw toUpstreamHttpError(outcome.status);
|
|
333
750
|
}
|
|
334
751
|
return outcome;
|
|
335
752
|
}
|
|
753
|
+
let lastError;
|
|
336
754
|
let lastErrorCode;
|
|
337
755
|
let lastStatus;
|
|
338
|
-
|
|
756
|
+
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
757
|
+
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
758
|
+
// increment it). Retry summaries and the status-retry budget must reflect
|
|
759
|
+
// issued requests, not the raw offset, so they stay accurate when partial
|
|
760
|
+
// allocations skip offsets.
|
|
761
|
+
let issued = 0;
|
|
762
|
+
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
763
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
764
|
+
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
765
|
+
// so the catch counts a thrown *transport* failure once without
|
|
766
|
+
// double-counting a status outcome that already incremented before it
|
|
767
|
+
// re-threw as an upstream HTTP error.
|
|
768
|
+
let issuedThisAttempt = false;
|
|
339
769
|
try {
|
|
340
770
|
const outcome = await executeOnce(attempt - 1);
|
|
771
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
772
|
+
// Duplicate endpoint from a partial allocation: advance the flat
|
|
773
|
+
// offset without issuing the request (no backoff, not a failure) so
|
|
774
|
+
// the loop keeps rotating toward the fallback vendor.
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
issued += 1;
|
|
778
|
+
issuedThisAttempt = true;
|
|
341
779
|
if (isHttpStatusOutcome(outcome)) {
|
|
342
780
|
lastStatus = outcome.status;
|
|
343
|
-
if (outcome.retryable &&
|
|
344
|
-
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
|
|
781
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
782
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), clientOptions.signal);
|
|
345
783
|
continue;
|
|
346
784
|
}
|
|
347
785
|
throw toUpstreamHttpError(outcome.status);
|
|
@@ -350,10 +788,10 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
350
788
|
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
351
789
|
throw toUpstreamHttpError(response.status);
|
|
352
790
|
}
|
|
353
|
-
if (
|
|
791
|
+
if (issued > 1) {
|
|
354
792
|
const summary = {
|
|
355
|
-
attempts:
|
|
356
|
-
retries:
|
|
793
|
+
attempts: issued,
|
|
794
|
+
retries: issued - 1,
|
|
357
795
|
...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
|
|
358
796
|
transport: "native",
|
|
359
797
|
...(lastErrorCode ? { lastErrorCode } : {}),
|
|
@@ -364,10 +802,14 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
364
802
|
return response;
|
|
365
803
|
}
|
|
366
804
|
catch (error) {
|
|
805
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
806
|
+
if (!issuedThisAttempt)
|
|
807
|
+
issued += 1;
|
|
808
|
+
lastError = error;
|
|
367
809
|
lastErrorCode = proxyTransportRetryErrorCode(error);
|
|
368
810
|
lastStatus = proxyTransportRetryErrorStatus(error);
|
|
369
811
|
const proxyUsed = Boolean(error.proxyUsed);
|
|
370
|
-
if (attempt <
|
|
812
|
+
if (attempt < transportAttemptCap &&
|
|
371
813
|
shouldRetryProxyTransportAttempt({
|
|
372
814
|
error,
|
|
373
815
|
explicitRetry,
|
|
@@ -375,23 +817,43 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
375
817
|
options: retryOptions,
|
|
376
818
|
proxyUsed,
|
|
377
819
|
})) {
|
|
378
|
-
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt));
|
|
820
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), clientOptions.signal);
|
|
379
821
|
continue;
|
|
380
822
|
}
|
|
381
823
|
throw error;
|
|
382
824
|
}
|
|
383
825
|
}
|
|
826
|
+
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
827
|
+
// e.g. the final offsets of a partial allocation all resolved to
|
|
828
|
+
// already-tried endpoints and were skipped. Surface the last real transport
|
|
829
|
+
// failure rather than a synthetic exhaustion error.
|
|
830
|
+
if (lastError !== undefined) {
|
|
831
|
+
throw lastError;
|
|
832
|
+
}
|
|
384
833
|
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
385
834
|
code: "retry_exhausted",
|
|
386
835
|
});
|
|
387
836
|
}
|
|
388
837
|
async function streamRequest(url, method, options = {}) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
838
|
+
const { headersOptions, methodName } = (() => {
|
|
839
|
+
try {
|
|
840
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
841
|
+
throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
|
|
842
|
+
}
|
|
843
|
+
assertNoHttpTransportOverrides(options);
|
|
844
|
+
const redirectPolicy = snapshotHttpRedirectPolicy(options);
|
|
845
|
+
return {
|
|
846
|
+
headersOptions: {
|
|
847
|
+
...withClientHeaders(options, clientOptions, options.body),
|
|
848
|
+
redirectPolicy,
|
|
849
|
+
},
|
|
850
|
+
methodName: normalizeHttpMethod(method),
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
catch (error) {
|
|
854
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
855
|
+
}
|
|
856
|
+
})();
|
|
395
857
|
return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
|
|
396
858
|
}
|
|
397
859
|
return {
|