@apifuse/provider-sdk 2.2.0-beta.2 → 2.2.0-beta.21
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 +487 -0
- package/CHANGELOG.md +90 -0
- package/README.md +49 -3
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +44 -59
- package/bin/apifuse-create.ts +1 -1
- package/bin/apifuse-dev.ts +27 -52
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-pack-smoke.ts +36 -81
- package/bin/apifuse-pack-types.ts +305 -0
- package/bin/apifuse-perf.ts +45 -127
- package/bin/apifuse-record.ts +659 -111
- package/bin/apifuse-submit-check.ts +546 -48
- package/bin/apifuse-sync-assets.ts +117 -0
- package/bin/apifuse.ts +1 -1
- package/bin/submit-check-delimited-text.ts +50 -0
- package/bin/submit-check-xml-semantics.ts +204 -0
- package/bin/submit-check-xml.ts +134 -0
- package/dist/auth-turn/index.d.ts +3 -3
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +9 -18
- package/dist/ceremonies/index.d.ts +9 -1
- package/dist/ceremonies/index.js +65 -18
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- 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/config/loader.d.ts +176 -8
- package/dist/config/loader.js +424 -95
- package/dist/contract-serialization.d.ts +2 -2
- package/dist/contract-serialization.js +3 -6
- package/dist/contract-types.d.ts +2 -2
- package/dist/contract.d.ts +3 -3
- package/dist/contract.js +4 -6
- package/dist/define.d.ts +9 -1
- package/dist/define.js +304 -119
- package/dist/dev.d.ts +1 -1
- package/dist/dev.js +1 -1
- package/dist/error-resolution.d.ts +2 -0
- package/dist/error-resolution.js +90 -0
- package/dist/errors.d.ts +22 -1
- package/dist/errors.js +88 -0
- package/dist/fixture-sanitization.d.ts +26 -0
- package/dist/fixture-sanitization.js +216 -0
- package/dist/i18n/catalog.d.ts +2 -2
- package/dist/i18n/catalog.js +4 -10
- package/dist/i18n/index.d.ts +2 -2
- package/dist/i18n/index.js +2 -2
- package/dist/i18n/keys.d.ts +2 -2
- package/dist/index.d.ts +46 -42
- package/dist/index.js +41 -37
- package/dist/lint.d.ts +1 -1
- package/dist/lint.js +8 -15
- 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 +12 -11
- package/dist/provider.js +10 -9
- package/dist/public-schema-field-lint.d.ts +1 -1
- package/dist/recipes/gov-api.js +1 -1
- package/dist/runtime/auth-flow.d.ts +2 -1
- package/dist/runtime/auth-flow.js +4 -3
- package/dist/runtime/browser.d.ts +1 -1
- package/dist/runtime/browser.js +15 -29
- package/dist/runtime/cache.d.ts +1 -1
- package/dist/runtime/cache.js +4 -8
- package/dist/runtime/choice.d.ts +1 -1
- package/dist/runtime/choice.js +31 -35
- package/dist/runtime/credential.d.ts +1 -1
- package/dist/runtime/credential.js +1 -1
- package/dist/runtime/env.d.ts +1 -1
- package/dist/runtime/executor.d.ts +1 -1
- package/dist/runtime/executor.js +31 -4
- package/dist/runtime/http.d.ts +2 -2
- package/dist/runtime/http.js +387 -47
- package/dist/runtime/insights.d.ts +1 -1
- package/dist/runtime/insights.js +6 -13
- package/dist/runtime/instrumentation.d.ts +2 -2
- package/dist/runtime/instrumentation.js +345 -22
- package/dist/runtime/keyring.js +1 -1
- package/dist/runtime/namespace.js +1 -1
- package/dist/runtime/native-network.d.ts +127 -0
- package/dist/runtime/native-network.js +1298 -0
- package/dist/runtime/otlp.d.ts +1 -1
- package/dist/runtime/perf.d.ts +1 -1
- package/dist/runtime/provider.d.ts +1 -1
- package/dist/runtime/provider.js +1 -2
- package/dist/runtime/proxy-errors.d.ts +1 -1
- package/dist/runtime/proxy-errors.js +9 -7
- package/dist/runtime/proxy-nodemaven.d.ts +56 -0
- package/dist/runtime/proxy-nodemaven.js +146 -0
- package/dist/runtime/proxy-retry-policy.d.ts +2 -2
- package/dist/runtime/proxy-retry-policy.js +2 -2
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +55 -52
- 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 +2 -4
- package/dist/runtime/request-options.d.ts +68 -1
- package/dist/runtime/request-options.js +548 -0
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/state.d.ts +2 -2
- package/dist/runtime/state.js +238 -26
- package/dist/runtime/stealth.d.ts +5 -3
- package/dist/runtime/stealth.js +423 -143
- package/dist/runtime/stt.d.ts +1 -1
- package/dist/runtime/stt.js +11 -15
- package/dist/runtime/trace.d.ts +2 -2
- package/dist/runtime/trace.js +2 -4
- package/dist/runtime/waterfall.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +7 -15
- package/dist/serve.d.ts +1 -1
- package/dist/serve.js +1 -1
- package/dist/server/index.d.ts +7 -7
- package/dist/server/index.js +6 -6
- 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.d.ts +1 -1
- package/dist/server/self-test-redaction.js +1 -1
- package/dist/server/self-test.d.ts +104 -3
- package/dist/server/self-test.js +673 -115
- package/dist/server/serve.d.ts +116 -4
- package/dist/server/serve.js +799 -128
- package/dist/server/types.d.ts +34 -9
- package/dist/server/types.js +8 -1
- package/dist/stateful/errors.d.ts +14 -0
- package/dist/stateful/errors.js +14 -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 +71 -0
- package/dist/stateful/stateful-provider-session-routing.js +353 -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 +1 -1
- package/dist/stealth/profiles.js +5 -14
- package/dist/stream-evidence.d.ts +74 -0
- package/dist/stream-evidence.js +785 -0
- package/dist/stream.d.ts +1 -1
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/testing/run.d.ts +32 -2
- package/dist/testing/run.js +478 -28
- package/dist/types.d.ts +342 -14
- package/dist/types.js +1 -0
- package/dist/user-input.d.ts +30 -0
- package/dist/user-input.js +66 -0
- package/package.json +16 -5
- package/src/auth-turn/index.ts +1 -1
- package/src/auth.ts +28 -86
- package/src/ceremonies/index.ts +103 -78
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +652 -204
- package/src/contract-serialization.ts +5 -11
- package/src/contract-types.ts +2 -2
- package/src/contract.ts +12 -28
- package/src/define.ts +499 -498
- package/src/dev.ts +4 -9
- package/src/error-resolution.ts +91 -0
- package/src/errors.ts +110 -5
- package/src/fixture-sanitization.ts +247 -0
- package/src/i18n/catalog.ts +10 -32
- package/src/i18n/index.ts +2 -2
- package/src/i18n/keys.ts +5 -11
- package/src/index.ts +112 -42
- package/src/lint.ts +88 -152
- 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 +66 -11
- package/src/public-schema-field-lint.ts +7 -33
- package/src/recipes/gov-api.ts +2 -5
- package/src/runtime/auth-flow.ts +7 -7
- package/src/runtime/browser.ts +64 -187
- package/src/runtime/cache.ts +20 -67
- package/src/runtime/choice.ts +79 -132
- package/src/runtime/credential.ts +2 -2
- package/src/runtime/env.ts +1 -1
- package/src/runtime/executor.ts +43 -20
- package/src/runtime/http.ts +494 -57
- package/src/runtime/insights.ts +15 -53
- package/src/runtime/instrumentation.ts +495 -66
- package/src/runtime/keyring.ts +7 -19
- package/src/runtime/namespace.ts +2 -7
- package/src/runtime/native-network.ts +1686 -0
- package/src/runtime/otlp.ts +12 -23
- package/src/runtime/perf.ts +1 -1
- package/src/runtime/provider.ts +4 -9
- package/src/runtime/proxy-errors.ts +29 -42
- package/src/runtime/proxy-nodemaven.ts +221 -0
- package/src/runtime/proxy-retry-policy.ts +3 -3
- package/src/runtime/proxy-telemetry.ts +79 -77
- package/src/runtime/redirects.ts +66 -0
- package/src/runtime/redis.ts +4 -12
- package/src/runtime/request-options.ts +679 -9
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/state.ts +353 -133
- package/src/runtime/stealth.ts +505 -154
- package/src/runtime/stt.ts +38 -94
- package/src/runtime/trace.ts +14 -44
- package/src/runtime/waterfall.ts +5 -18
- package/src/schema.ts +23 -84
- package/src/serve.ts +1 -1
- package/src/server/index.ts +29 -7
- package/src/server/self-test-input-tokens.ts +29 -14
- package/src/server/self-test-redaction.ts +2 -2
- package/src/server/self-test.ts +857 -132
- package/src/server/serve.ts +1151 -328
- package/src/server/types.ts +12 -13
- package/src/stateful/README.md +146 -0
- package/src/stateful/errors.ts +23 -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 +555 -0
- package/src/stateful/stateful-provider-session-runtime.ts +403 -0
- package/src/stateful-signing.ts +46 -0
- package/src/stealth/profiles.ts +10 -26
- package/src/stream-evidence.ts +988 -0
- package/src/stream.ts +8 -19
- package/src/testing/index.ts +10 -2
- package/src/testing/run.ts +653 -74
- package/src/types.ts +408 -28
- package/src/user-input.ts +118 -0
- 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,12 +1,16 @@
|
|
|
1
|
-
import { resolveProxyConfigAsync } from "../config/loader";
|
|
2
|
-
import { ProviderError, TransportError } from "../errors";
|
|
3
|
-
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream";
|
|
4
|
-
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy";
|
|
5
|
-
import {
|
|
1
|
+
import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, resolveProxyConfigAsync, } from "../config/loader.js";
|
|
2
|
+
import { HttpRedirectError, ProviderError, TransportError } from "../errors.js";
|
|
3
|
+
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
4
|
+
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
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
|
}
|
|
11
|
+
function isDedupeSkipOutcome(outcome) {
|
|
12
|
+
return "kind" in outcome && outcome.kind === "dedupe-skip";
|
|
13
|
+
}
|
|
10
14
|
async function sleep(ms) {
|
|
11
15
|
if (ms <= 0)
|
|
12
16
|
return;
|
|
@@ -121,9 +125,38 @@ function requireNativeResponseBody(response) {
|
|
|
121
125
|
}
|
|
122
126
|
return response.body;
|
|
123
127
|
}
|
|
124
|
-
function
|
|
128
|
+
function sanitizeStreamErrors(body, serializedUrl) {
|
|
129
|
+
if (serializedUrl.sensitiveValues.length === 0)
|
|
130
|
+
return body;
|
|
131
|
+
let reader;
|
|
132
|
+
return new ReadableStream({
|
|
133
|
+
async pull(controller) {
|
|
134
|
+
try {
|
|
135
|
+
reader ??= body.getReader();
|
|
136
|
+
const chunk = await reader.read();
|
|
137
|
+
if (chunk.done) {
|
|
138
|
+
controller.close();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
controller.enqueue(chunk.value);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
controller.error(redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl));
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
async cancel(reason) {
|
|
148
|
+
try {
|
|
149
|
+
await (reader ? reader.cancel(reason) : body.cancel(reason));
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
}, { highWaterMark: 0 });
|
|
156
|
+
}
|
|
157
|
+
function toNativeHttpStreamResponse(response, serializedUrl) {
|
|
125
158
|
const headers = Object.fromEntries(response.headers.entries());
|
|
126
|
-
const body = requireNativeResponseBody(response);
|
|
159
|
+
const body = sanitizeStreamErrors(requireNativeResponseBody(response), serializedUrl);
|
|
127
160
|
return {
|
|
128
161
|
body,
|
|
129
162
|
headers,
|
|
@@ -164,6 +197,181 @@ function isAbsoluteUrl(url) {
|
|
|
164
197
|
function resolveHttpUrl(baseUrl, url) {
|
|
165
198
|
return new URL(url, baseUrl ?? DEFAULT_HTTP_BASE_URL).toString();
|
|
166
199
|
}
|
|
200
|
+
const MAX_HTTP_REDIRECT_HOPS = 20;
|
|
201
|
+
const HTTP_REDIRECT_POLICY_FIELDS = new Set(["mode", "maxHops"]);
|
|
202
|
+
const REDIRECT_BODY_HEADERS = new Set([
|
|
203
|
+
"content-encoding",
|
|
204
|
+
"content-language",
|
|
205
|
+
"content-location",
|
|
206
|
+
"content-type",
|
|
207
|
+
]);
|
|
208
|
+
const MALFORMED_REDIRECT_TARGET = "[malformed redirect target]";
|
|
209
|
+
function invalidHttpRedirectPolicy(message, cause) {
|
|
210
|
+
return new TransportError(`Invalid ctx.http redirectPolicy: ${message}`, {
|
|
211
|
+
code: "http_redirect_policy_invalid",
|
|
212
|
+
...(cause ? { cause } : {}),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
/** Snapshot untrusted caller input synchronously, before proxy resolution or fetch. */
|
|
216
|
+
function normalizeHttpRedirectPolicy(value) {
|
|
217
|
+
if (value === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
220
|
+
throw invalidHttpRedirectPolicy("expected an object");
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const keys = Reflect.ownKeys(value);
|
|
224
|
+
for (const key of keys) {
|
|
225
|
+
if (typeof key !== "string" || !HTTP_REDIRECT_POLICY_FIELDS.has(key)) {
|
|
226
|
+
throw invalidHttpRedirectPolicy(`unknown field ${String(key)}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const field of HTTP_REDIRECT_POLICY_FIELDS) {
|
|
230
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, field);
|
|
231
|
+
if (!descriptor || !("value" in descriptor)) {
|
|
232
|
+
throw invalidHttpRedirectPolicy(`${field} must be an own data property`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const record = value;
|
|
236
|
+
if (record.mode !== "same-origin") {
|
|
237
|
+
throw invalidHttpRedirectPolicy('mode must be "same-origin"');
|
|
238
|
+
}
|
|
239
|
+
if (typeof record.maxHops !== "number" ||
|
|
240
|
+
!Number.isInteger(record.maxHops) ||
|
|
241
|
+
record.maxHops < 0 ||
|
|
242
|
+
record.maxHops > MAX_HTTP_REDIRECT_HOPS) {
|
|
243
|
+
throw invalidHttpRedirectPolicy(`maxHops must be an integer from 0 to ${MAX_HTTP_REDIRECT_HOPS}`);
|
|
244
|
+
}
|
|
245
|
+
return { mode: "same-origin", maxHops: record.maxHops };
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
if (error instanceof TransportError)
|
|
249
|
+
throw error;
|
|
250
|
+
throw invalidHttpRedirectPolicy("could not be inspected safely", error instanceof Error ? error : undefined);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function snapshotHttpRedirectPolicy(options) {
|
|
254
|
+
try {
|
|
255
|
+
return normalizeHttpRedirectPolicy(options.redirectPolicy);
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (error instanceof TransportError)
|
|
259
|
+
throw error;
|
|
260
|
+
throw invalidHttpRedirectPolicy("could not be read safely", error instanceof Error ? error : undefined);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function withoutRedirectBodyHeaders(headers) {
|
|
264
|
+
const nextHeaders = new Headers(headers);
|
|
265
|
+
for (const name of REDIRECT_BODY_HEADERS)
|
|
266
|
+
nextHeaders.delete(name);
|
|
267
|
+
return nextHeaders;
|
|
268
|
+
}
|
|
269
|
+
function redirectDiagnosticTarget(value) {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = new URL(value);
|
|
272
|
+
// Origin omits URL userinfo. Keeping only origin + path makes diagnostics
|
|
273
|
+
// useful while structurally excluding every query value and fragment,
|
|
274
|
+
// including attacker-chosen keys the provider did not declare sensitive.
|
|
275
|
+
const redactedQuery = parsed.search ? "?[REDACTED]" : "";
|
|
276
|
+
return parsed.origin === "null"
|
|
277
|
+
? `${parsed.protocol}<opaque-target>`
|
|
278
|
+
: `${parsed.origin}${parsed.pathname}${redactedQuery}`;
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return MALFORMED_REDIRECT_TARGET;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function discardRedirectResponseBody(response) {
|
|
285
|
+
try {
|
|
286
|
+
const cancellation = response.body?.cancel();
|
|
287
|
+
if (cancellation)
|
|
288
|
+
void cancellation.catch(() => undefined);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
// The redirect decision is security-significant and must not be replaced
|
|
292
|
+
// or delayed by an upstream body's cancellation failure. Cancellation was
|
|
293
|
+
// attempted; redirect evaluation continues without awaiting its completion.
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
async function fetchWithHttpRedirectPolicy(requestUrl, requestInit, policy) {
|
|
297
|
+
if (!policy)
|
|
298
|
+
return fetch(requestUrl, requestInit);
|
|
299
|
+
const initialUrl = new URL(requestUrl);
|
|
300
|
+
if (initialUrl.protocol !== "http:" && initialUrl.protocol !== "https:") {
|
|
301
|
+
throw new TransportError("ctx.http redirectPolicy requires an HTTP(S) origin", {
|
|
302
|
+
code: "transport_invalid_url",
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
const initialOrigin = initialUrl.origin;
|
|
306
|
+
let currentUrl = requestUrl;
|
|
307
|
+
let method = normalizeHttpMethod(requestInit.method ?? "GET");
|
|
308
|
+
let body = requestInit.body;
|
|
309
|
+
let headers = requestInit.headers;
|
|
310
|
+
let followedHops = 0;
|
|
311
|
+
const visitedRequests = new Set([`${method} ${currentUrl}`]);
|
|
312
|
+
while (true) {
|
|
313
|
+
const response = await fetch(currentUrl, {
|
|
314
|
+
...requestInit,
|
|
315
|
+
body,
|
|
316
|
+
headers,
|
|
317
|
+
method,
|
|
318
|
+
redirect: "manual",
|
|
319
|
+
});
|
|
320
|
+
if (!isRedirectStatus(response.status))
|
|
321
|
+
return response;
|
|
322
|
+
const location = response.headers.get("location");
|
|
323
|
+
discardRedirectResponseBody(response);
|
|
324
|
+
let nextUrlString;
|
|
325
|
+
try {
|
|
326
|
+
nextUrlString = resolveRedirectUrl(location || undefined, currentUrl);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
const target = MALFORMED_REDIRECT_TARGET;
|
|
330
|
+
throw new HttpRedirectError(`Redirect response has malformed Location target ${target}`, {
|
|
331
|
+
reason: "missing_location",
|
|
332
|
+
target,
|
|
333
|
+
status: response.status,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const decision = evaluateRedirectHop({
|
|
337
|
+
status: response.status,
|
|
338
|
+
method,
|
|
339
|
+
nextUrl: nextUrlString,
|
|
340
|
+
shouldStop: nextUrlString ? new URL(nextUrlString).origin !== initialOrigin : false,
|
|
341
|
+
redirectCount: followedHops + 1,
|
|
342
|
+
maxHops: policy.maxHops,
|
|
343
|
+
visitedRequests,
|
|
344
|
+
});
|
|
345
|
+
if (decision.kind === "stop") {
|
|
346
|
+
const target = decision.nextUrl ? redirectDiagnosticTarget(decision.nextUrl) : undefined;
|
|
347
|
+
const message = (() => {
|
|
348
|
+
switch (decision.reason) {
|
|
349
|
+
case "stopped":
|
|
350
|
+
return `Redirect policy refused cross-origin target ${target}`;
|
|
351
|
+
case "max_hops":
|
|
352
|
+
return `Redirect policy reached maxHops before target ${target}`;
|
|
353
|
+
case "loop":
|
|
354
|
+
return `Redirect loop refused target ${target}`;
|
|
355
|
+
case "missing_location":
|
|
356
|
+
return `Redirect response from ${redirectDiagnosticTarget(currentUrl)} is missing Location`;
|
|
357
|
+
}
|
|
358
|
+
})();
|
|
359
|
+
throw new HttpRedirectError(message, {
|
|
360
|
+
reason: decision.reason,
|
|
361
|
+
...(target ? { target } : {}),
|
|
362
|
+
status: response.status,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
if (decision.nextMethod !== method) {
|
|
366
|
+
body = undefined;
|
|
367
|
+
headers = withoutRedirectBodyHeaders(headers);
|
|
368
|
+
}
|
|
369
|
+
method = decision.nextMethod;
|
|
370
|
+
currentUrl = decision.nextUrl;
|
|
371
|
+
followedHops += 1;
|
|
372
|
+
visitedRequests.add(`${method} ${currentUrl}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
167
375
|
async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset = 0) {
|
|
168
376
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
169
377
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
@@ -175,6 +383,9 @@ async function resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffs
|
|
|
175
383
|
baseProxyAttempt: clientOptions.proxyAttempt,
|
|
176
384
|
retryAttemptOffset: proxyAttemptOffset,
|
|
177
385
|
}),
|
|
386
|
+
// Bun's native fetch proxy option tunnels HTTP CONNECT only; SOCKS5 is not
|
|
387
|
+
// supported here, so a socks5 policy fails loudly rather than downgrading.
|
|
388
|
+
transportProtocols: ["http"],
|
|
178
389
|
telemetry: clientOptions.telemetry,
|
|
179
390
|
});
|
|
180
391
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -196,15 +407,37 @@ function normalizeNativeFetchBody(body) {
|
|
|
196
407
|
copied.set(normalized);
|
|
197
408
|
return copied.buffer;
|
|
198
409
|
}
|
|
199
|
-
|
|
200
|
-
|
|
410
|
+
function serializeHttpRequestUrl(baseUrl, url, options) {
|
|
411
|
+
try {
|
|
412
|
+
return serializeRequestUrl(resolveHttpUrl(baseUrl, url), options.params, options.sensitiveParams);
|
|
413
|
+
}
|
|
414
|
+
catch (error) {
|
|
415
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
|
|
419
|
+
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
420
|
+
const { requestUrl } = serializedUrl;
|
|
201
421
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
202
422
|
const timeoutHandle = options.timeout
|
|
203
423
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
204
424
|
: undefined;
|
|
205
425
|
let proxy;
|
|
206
426
|
try {
|
|
427
|
+
// Resolve inside the try (and after the timeout is armed) so allocator
|
|
428
|
+
// failures are branded as TransportErrors and count against the request
|
|
429
|
+
// deadline, exactly as an inline resolve would.
|
|
207
430
|
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
431
|
+
// For a registry allocator chain, skip an endpoint a prior attempt already
|
|
432
|
+
// tried rather than re-issuing the same request. Returning the sentinel
|
|
433
|
+
// (instead of breaking) lets the loop keep advancing the flat offset until
|
|
434
|
+
// it crosses into the fallback vendor's pool span.
|
|
435
|
+
if (dedupe && proxy) {
|
|
436
|
+
if (dedupe.attempted.has(proxy)) {
|
|
437
|
+
return { kind: "dedupe-skip" };
|
|
438
|
+
}
|
|
439
|
+
dedupe.attempted.add(proxy);
|
|
440
|
+
}
|
|
208
441
|
const requestInit = {
|
|
209
442
|
headers: options.headers,
|
|
210
443
|
method,
|
|
@@ -214,9 +447,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
214
447
|
if (options.body !== undefined) {
|
|
215
448
|
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
216
449
|
}
|
|
217
|
-
const response = await
|
|
218
|
-
...requestInit,
|
|
219
|
-
});
|
|
450
|
+
const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
|
|
220
451
|
const headers = Object.fromEntries(response.headers.entries());
|
|
221
452
|
if (statusRetryCodes && response.status >= 400) {
|
|
222
453
|
await drainNativeResponseBody(response);
|
|
@@ -239,9 +470,9 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
239
470
|
}
|
|
240
471
|
catch (error) {
|
|
241
472
|
if (error instanceof SyntaxError) {
|
|
242
|
-
throw error;
|
|
473
|
+
throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
243
474
|
}
|
|
244
|
-
const transportError = toHttpTransportError(error);
|
|
475
|
+
const transportError = redactSensitiveError(toHttpTransportError(error), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
245
476
|
transportError.proxyUsed = Boolean(proxy);
|
|
246
477
|
throw transportError;
|
|
247
478
|
}
|
|
@@ -251,7 +482,8 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
251
482
|
}
|
|
252
483
|
}
|
|
253
484
|
async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn) {
|
|
254
|
-
const
|
|
485
|
+
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
486
|
+
const { requestUrl } = serializedUrl;
|
|
255
487
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
256
488
|
const timeoutHandle = options.timeout
|
|
257
489
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
@@ -267,9 +499,7 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
|
|
|
267
499
|
if (options.body !== undefined) {
|
|
268
500
|
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
269
501
|
}
|
|
270
|
-
const response = await
|
|
271
|
-
...requestInit,
|
|
272
|
-
});
|
|
502
|
+
const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
|
|
273
503
|
if (response.status >= 400 && options.throwOnHttpError !== false) {
|
|
274
504
|
await drainNativeResponseBody(response);
|
|
275
505
|
throw new TransportError(`Upstream request failed with status ${response.status}`, {
|
|
@@ -277,13 +507,13 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
|
|
|
277
507
|
status: response.status,
|
|
278
508
|
});
|
|
279
509
|
}
|
|
280
|
-
return toNativeHttpStreamResponse(response);
|
|
510
|
+
return toNativeHttpStreamResponse(response, serializedUrl);
|
|
281
511
|
}
|
|
282
512
|
catch (error) {
|
|
283
513
|
if (error instanceof SyntaxError) {
|
|
284
|
-
throw error;
|
|
514
|
+
throw redactSensitiveError(error, serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
285
515
|
}
|
|
286
|
-
throw toHttpTransportError(error);
|
|
516
|
+
throw redactSensitiveError(toHttpTransportError(error), serializedUrl.sensitiveValues, serializedUrl.requestUrl, serializedUrl.redactedUrl);
|
|
287
517
|
}
|
|
288
518
|
finally {
|
|
289
519
|
if (timeoutHandle)
|
|
@@ -301,19 +531,31 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
301
531
|
warn(message);
|
|
302
532
|
};
|
|
303
533
|
async function request(url, method, options = {}) {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
534
|
+
const { explicitRetry, headersOptions, methodName, retryOptions } = (() => {
|
|
535
|
+
try {
|
|
536
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
537
|
+
throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
|
|
538
|
+
}
|
|
539
|
+
assertNoHttpTransportOverrides(options);
|
|
540
|
+
const redirectPolicy = snapshotHttpRedirectPolicy(options);
|
|
541
|
+
const headersOptions = {
|
|
542
|
+
...withClientHeaders(options, clientOptions, options.body),
|
|
543
|
+
redirectPolicy,
|
|
544
|
+
};
|
|
545
|
+
const methodName = normalizeHttpMethod(method);
|
|
546
|
+
const explicitRetry = headersOptions.retry !== undefined;
|
|
547
|
+
const retryOptions = normalizeProxyTransportRetryOptions(headersOptions.retry, {
|
|
548
|
+
label: "HTTP",
|
|
549
|
+
}) ??
|
|
550
|
+
(explicitRetry ? undefined : createDefaultProxyTransportRetryOptions({ label: "HTTP" }));
|
|
551
|
+
if (retryOptions)
|
|
552
|
+
validateUnsafeProxyTransportRetryMethods(retryOptions, "HTTP");
|
|
553
|
+
return { explicitRetry, headersOptions, methodName, retryOptions };
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
557
|
+
}
|
|
558
|
+
})();
|
|
317
559
|
const retryEnabled = Boolean(retryOptions &&
|
|
318
560
|
retryOptions.attempts > 1 &&
|
|
319
561
|
isProxyTransportRetryMethod(methodName, retryOptions));
|
|
@@ -325,22 +567,97 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
325
567
|
const attemptOptions = statusRetryEnabled
|
|
326
568
|
? { ...headersOptions, throwOnHttpError: false }
|
|
327
569
|
: headersOptions;
|
|
328
|
-
|
|
570
|
+
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
571
|
+
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
572
|
+
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
573
|
+
// spans), so a transport failure should advance to the next endpoint —
|
|
574
|
+
// potentially crossing into the fallback vendor — rather than stopping at
|
|
575
|
+
// the per-endpoint retry budget and stranding the request on the primary
|
|
576
|
+
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
577
|
+
// only for implicit, safe-method allocator requests; explicit retry
|
|
578
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
579
|
+
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
580
|
+
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
581
|
+
// the full span.
|
|
582
|
+
const policyProxy = (() => {
|
|
583
|
+
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
584
|
+
return policy && typeof policy === "object" ? policy : undefined;
|
|
585
|
+
})();
|
|
586
|
+
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
587
|
+
const transportAttemptCap = retryOptions
|
|
588
|
+
? resolvePolicyTransportAttemptCap({
|
|
589
|
+
policy: policyProxy,
|
|
590
|
+
usesPolicyAllocator,
|
|
591
|
+
retryAttempts: retryOptions.attempts,
|
|
592
|
+
explicitRetry,
|
|
593
|
+
method: methodName,
|
|
594
|
+
})
|
|
595
|
+
: 1;
|
|
596
|
+
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
597
|
+
// attempts rotate the flat offset across the concatenated vendor pool
|
|
598
|
+
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
599
|
+
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
600
|
+
// the offset reaches the next vendor. Rather than re-hammering an
|
|
601
|
+
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
602
|
+
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
603
|
+
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
604
|
+
// vendor's pool span instead of stalling on the primary vendor.
|
|
605
|
+
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
606
|
+
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
607
|
+
// an explicit retry policy: there the caller's `attempts` count is the
|
|
608
|
+
// contract and each attempt must issue against whatever endpoint it resolves
|
|
609
|
+
// — even a repeat — instead of being silently skipped (which would collapse
|
|
610
|
+
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
611
|
+
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
612
|
+
policy: policyProxy,
|
|
613
|
+
usesPolicyAllocator,
|
|
614
|
+
explicitRetry,
|
|
615
|
+
method: methodName,
|
|
616
|
+
});
|
|
617
|
+
const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
|
|
618
|
+
const executeOnce = (proxyAttemptOffset = 0) => fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext);
|
|
329
619
|
if (!retryEnabled || !retryOptions) {
|
|
330
620
|
const outcome = await executeOnce();
|
|
621
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
622
|
+
// Single-shot path never de-duplicates (dedupeContext is undefined),
|
|
623
|
+
// but keep the union total.
|
|
624
|
+
throw new TransportError("HTTP request produced no terminal result", {
|
|
625
|
+
code: "retry_exhausted",
|
|
626
|
+
});
|
|
627
|
+
}
|
|
331
628
|
if (isHttpStatusOutcome(outcome)) {
|
|
332
629
|
throw toUpstreamHttpError(outcome.status);
|
|
333
630
|
}
|
|
334
631
|
return outcome;
|
|
335
632
|
}
|
|
633
|
+
let lastError;
|
|
336
634
|
let lastErrorCode;
|
|
337
635
|
let lastStatus;
|
|
338
|
-
|
|
636
|
+
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
637
|
+
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
638
|
+
// increment it). Retry summaries and the status-retry budget must reflect
|
|
639
|
+
// issued requests, not the raw offset, so they stay accurate when partial
|
|
640
|
+
// allocations skip offsets.
|
|
641
|
+
let issued = 0;
|
|
642
|
+
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
643
|
+
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
644
|
+
// so the catch counts a thrown *transport* failure once without
|
|
645
|
+
// double-counting a status outcome that already incremented before it
|
|
646
|
+
// re-threw as an upstream HTTP error.
|
|
647
|
+
let issuedThisAttempt = false;
|
|
339
648
|
try {
|
|
340
649
|
const outcome = await executeOnce(attempt - 1);
|
|
650
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
651
|
+
// Duplicate endpoint from a partial allocation: advance the flat
|
|
652
|
+
// offset without issuing the request (no backoff, not a failure) so
|
|
653
|
+
// the loop keeps rotating toward the fallback vendor.
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
issued += 1;
|
|
657
|
+
issuedThisAttempt = true;
|
|
341
658
|
if (isHttpStatusOutcome(outcome)) {
|
|
342
659
|
lastStatus = outcome.status;
|
|
343
|
-
if (outcome.retryable &&
|
|
660
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
344
661
|
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers));
|
|
345
662
|
continue;
|
|
346
663
|
}
|
|
@@ -350,10 +667,10 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
350
667
|
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
351
668
|
throw toUpstreamHttpError(response.status);
|
|
352
669
|
}
|
|
353
|
-
if (
|
|
670
|
+
if (issued > 1) {
|
|
354
671
|
const summary = {
|
|
355
|
-
attempts:
|
|
356
|
-
retries:
|
|
672
|
+
attempts: issued,
|
|
673
|
+
retries: issued - 1,
|
|
357
674
|
...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
|
|
358
675
|
transport: "native",
|
|
359
676
|
...(lastErrorCode ? { lastErrorCode } : {}),
|
|
@@ -364,10 +681,13 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
364
681
|
return response;
|
|
365
682
|
}
|
|
366
683
|
catch (error) {
|
|
684
|
+
if (!issuedThisAttempt)
|
|
685
|
+
issued += 1;
|
|
686
|
+
lastError = error;
|
|
367
687
|
lastErrorCode = proxyTransportRetryErrorCode(error);
|
|
368
688
|
lastStatus = proxyTransportRetryErrorStatus(error);
|
|
369
689
|
const proxyUsed = Boolean(error.proxyUsed);
|
|
370
|
-
if (attempt <
|
|
690
|
+
if (attempt < transportAttemptCap &&
|
|
371
691
|
shouldRetryProxyTransportAttempt({
|
|
372
692
|
error,
|
|
373
693
|
explicitRetry,
|
|
@@ -381,17 +701,37 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
381
701
|
throw error;
|
|
382
702
|
}
|
|
383
703
|
}
|
|
704
|
+
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
705
|
+
// e.g. the final offsets of a partial allocation all resolved to
|
|
706
|
+
// already-tried endpoints and were skipped. Surface the last real transport
|
|
707
|
+
// failure rather than a synthetic exhaustion error.
|
|
708
|
+
if (lastError !== undefined) {
|
|
709
|
+
throw lastError;
|
|
710
|
+
}
|
|
384
711
|
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
385
712
|
code: "retry_exhausted",
|
|
386
713
|
});
|
|
387
714
|
}
|
|
388
715
|
async function streamRequest(url, method, options = {}) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
716
|
+
const { headersOptions, methodName } = (() => {
|
|
717
|
+
try {
|
|
718
|
+
if (!baseUrl && !isAbsoluteUrl(url)) {
|
|
719
|
+
throw new TransportError("ctx.http requires an absolute URL when provider.upstream.baseUrl is not declared", { code: "transport_invalid_url" });
|
|
720
|
+
}
|
|
721
|
+
assertNoHttpTransportOverrides(options);
|
|
722
|
+
const redirectPolicy = snapshotHttpRedirectPolicy(options);
|
|
723
|
+
return {
|
|
724
|
+
headersOptions: {
|
|
725
|
+
...withClientHeaders(options, clientOptions, options.body),
|
|
726
|
+
redirectPolicy,
|
|
727
|
+
},
|
|
728
|
+
methodName: normalizeHttpMethod(method),
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
catch (error) {
|
|
732
|
+
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
733
|
+
}
|
|
734
|
+
})();
|
|
395
735
|
return fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce);
|
|
396
736
|
}
|
|
397
737
|
return {
|
package/dist/runtime/insights.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { computePercentile } from "./perf";
|
|
1
|
+
import { computePercentile } from "./perf.js";
|
|
2
2
|
const LARGE_RESPONSE_BYTES = 100_000;
|
|
3
3
|
const SLOW_TRANSFORM_MS = 10;
|
|
4
4
|
const DNS_WARN_MS = 5;
|
|
@@ -188,8 +188,7 @@ function getDnsRepeatedCandidate(spans) {
|
|
|
188
188
|
if (entry.totalCount < 2) {
|
|
189
189
|
continue;
|
|
190
190
|
}
|
|
191
|
-
const avgDnsMs = entry.dnsDurations.reduce((sum, value) => sum + value, 0) /
|
|
192
|
-
entry.dnsDurations.length;
|
|
191
|
+
const avgDnsMs = entry.dnsDurations.reduce((sum, value) => sum + value, 0) / entry.dnsDurations.length;
|
|
193
192
|
const reuseRate = entry.reuseCount / entry.totalCount;
|
|
194
193
|
if (avgDnsMs > DNS_WARN_MS && reuseRate < 0.2) {
|
|
195
194
|
if (!candidate || avgDnsMs > candidate.avgDnsMs) {
|
|
@@ -218,9 +217,7 @@ function getDnsRepeatedInsight(spans) {
|
|
|
218
217
|
}
|
|
219
218
|
function getProxyOverheadInsight(spans) {
|
|
220
219
|
const requestSpans = spans.filter(isRequestSpan);
|
|
221
|
-
const proxiedDurations = requestSpans
|
|
222
|
-
.filter(hasProxy)
|
|
223
|
-
.map((span) => span.duration_ms);
|
|
220
|
+
const proxiedDurations = requestSpans.filter(hasProxy).map((span) => span.duration_ms);
|
|
224
221
|
const directDurations = requestSpans
|
|
225
222
|
.filter((span) => !hasProxy(span))
|
|
226
223
|
.map((span) => span.duration_ms);
|
|
@@ -230,10 +227,8 @@ function getProxyOverheadInsight(spans) {
|
|
|
230
227
|
message: "✓ Proxy overhead: insufficient proxy/direct samples",
|
|
231
228
|
};
|
|
232
229
|
}
|
|
233
|
-
const proxyAvg = proxiedDurations.reduce((sum, value) => sum + value, 0) /
|
|
234
|
-
|
|
235
|
-
const directAvg = directDurations.reduce((sum, value) => sum + value, 0) /
|
|
236
|
-
directDurations.length;
|
|
230
|
+
const proxyAvg = proxiedDurations.reduce((sum, value) => sum + value, 0) / proxiedDurations.length;
|
|
231
|
+
const directAvg = directDurations.reduce((sum, value) => sum + value, 0) / directDurations.length;
|
|
237
232
|
if (directAvg > 0 && proxyAvg >= directAvg * 2) {
|
|
238
233
|
return {
|
|
239
234
|
triggered: true,
|
|
@@ -254,9 +249,7 @@ function getBrowserIdleInsight(spans) {
|
|
|
254
249
|
if (waitMs !== undefined) {
|
|
255
250
|
return waitMs;
|
|
256
251
|
}
|
|
257
|
-
return span.name.toLowerCase().includes("wait")
|
|
258
|
-
? span.duration_ms
|
|
259
|
-
: undefined;
|
|
252
|
+
return span.name.toLowerCase().includes("wait") ? span.duration_ms : undefined;
|
|
260
253
|
})
|
|
261
254
|
.filter((value) => value !== undefined);
|
|
262
255
|
if (waits.length === 0) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ProviderContext } from "../types";
|
|
2
|
-
import { type CreateTraceContextOptions, type TraceContext } from "./trace";
|
|
1
|
+
import type { ProviderContext } from "../types.js";
|
|
2
|
+
import { type CreateTraceContextOptions, type TraceContext } from "./trace.js";
|
|
3
3
|
export interface InstrumentationOptions extends CreateTraceContextOptions {
|
|
4
4
|
}
|
|
5
5
|
export type InstrumentedProviderContext<T extends ProviderContext> = Omit<T, "trace"> & {
|