@apifuse/provider-sdk 2.2.0-beta.60 → 2.2.0-beta.62
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 +17 -16
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +5 -7
- package/bin/apifuse-pack-types.ts +26 -0
- package/bin/apifuse-perf.ts +4 -6
- package/bin/apifuse-record.ts +4 -7
- package/bin/apifuse-submit-check.ts +2 -2
- package/dist/define.d.ts +3 -2
- package/dist/define.js +1 -1
- package/dist/dev.js +3 -1
- package/dist/engine.d.ts +7 -6
- package/dist/engine.js +3 -4
- package/dist/error-resolution.js +21 -4
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +9 -0
- package/dist/handle-meta.d.ts +55 -0
- package/dist/handle-meta.js +57 -0
- package/dist/handle.d.ts +208 -0
- package/dist/handle.js +299 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/lint.d.ts +3 -0
- package/dist/lint.js +372 -47
- package/dist/provider.d.ts +5 -3
- package/dist/provider.js +4 -2
- package/dist/runtime/handle-wordlist.d.ts +43 -0
- package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +900 -0
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +6 -0
- package/dist/runtime/http-telemetry-guard.js +150 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +222 -146
- package/dist/runtime/native-network.d.ts +10 -1
- package/dist/runtime/native-network.js +230 -29
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -0
- package/dist/runtime/state.d.ts +4 -1
- package/dist/runtime/state.js +32 -17
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +0 -3
- package/dist/server/serve-implementation.d.ts +6 -11
- package/dist/server/serve-implementation.js +119 -52
- package/dist/testing/run.js +10 -7
- package/dist/types.d.ts +4 -107
- package/package.json +1 -1
- package/src/define.ts +4 -3
- package/src/dev.ts +3 -1
- package/src/engine.ts +5 -6
- package/src/error-resolution.ts +21 -4
- package/src/errors.ts +13 -0
- package/src/handle-meta.ts +93 -0
- package/src/handle.ts +612 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/index.ts +45 -13
- package/src/lint.ts +437 -55
- package/src/provider.ts +19 -20
- package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
- package/src/runtime/handle.ts +1172 -0
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +155 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +295 -173
- package/src/runtime/native-network.ts +281 -33
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/state.ts +44 -16
- package/src/server/index.ts +30 -0
- package/src/server/sensitive-values.ts +0 -3
- package/src/server/serve-implementation.ts +127 -89
- package/src/testing/run.ts +11 -7
- package/src/types.ts +4 -137
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice-wordlist.d.ts +0 -9
- package/dist/runtime/choice.d.ts +0 -27
- package/dist/runtime/choice.js +0 -801
- package/src/choice-token.ts +0 -165
- package/src/runtime/choice.ts +0 -1197
package/dist/runtime/http.js
CHANGED
|
@@ -2,9 +2,34 @@ import { policyRotatesTransportVendorChain, resolvePolicyTransportAttemptCap, re
|
|
|
2
2
|
import { HttpRedirectError, ProviderError, TransportError } from "../errors.js";
|
|
3
3
|
import { parseSseStream, readableBytes, readableLines, readableTextChunks } from "../stream.js";
|
|
4
4
|
import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefaultProxyTransportRetryOptions, isProxyTransportRetryMethod, normalizeProxyTransportRetryOptions, proxyTransportRetryErrorCode, proxyTransportRetryErrorStatus, shouldRetryProxyTransportAttempt, validateUnsafeProxyTransportRetryMethods, } from "./proxy-retry-policy.js";
|
|
5
|
+
import { httpTelemetryErrorCode, } from "./http-telemetry.js";
|
|
5
6
|
import { evaluateRedirectHop, isRedirectStatus, resolveRedirectUrl } from "./redirects.js";
|
|
6
7
|
import { normalizeHttpRequestBody, redactSensitiveError, redactSensitiveRequestError, serializeRequestUrl, } from "./request-options.js";
|
|
8
|
+
import { startHttpTelemetry } from "./http-telemetry-guard.js";
|
|
9
|
+
import { scopedHttpTelemetry } from "./http-telemetry-binding.js";
|
|
7
10
|
const DEFAULT_HTTP_BASE_URL = "http://localhost";
|
|
11
|
+
function httpAttemptFailure(error, status) {
|
|
12
|
+
try {
|
|
13
|
+
return {
|
|
14
|
+
e: httpTelemetryErrorCode(proxyTransportRetryErrorCode(error)),
|
|
15
|
+
status: proxyTransportRetryErrorStatus(error) ?? status,
|
|
16
|
+
...(error instanceof Error
|
|
17
|
+
? {
|
|
18
|
+
diagnostics: {
|
|
19
|
+
name: error.name,
|
|
20
|
+
message: error.message,
|
|
21
|
+
...(error.cause instanceof Error
|
|
22
|
+
? { cause: { name: error.cause.name, message: error.cause.message } }
|
|
23
|
+
: {}),
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
: {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return { e: "other" };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
8
33
|
function isHttpStatusOutcome(outcome) {
|
|
9
34
|
return "kind" in outcome && outcome.kind === "http-status";
|
|
10
35
|
}
|
|
@@ -527,7 +552,7 @@ function serializeHttpRequestUrl(baseUrl, url, options) {
|
|
|
527
552
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
528
553
|
}
|
|
529
554
|
}
|
|
530
|
-
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe) {
|
|
555
|
+
async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, warn, statusRetryCodes, proxyAttemptOffset = 0, dedupe, observeProxy) {
|
|
531
556
|
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
532
557
|
const { requestUrl } = serializedUrl;
|
|
533
558
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
@@ -542,6 +567,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
542
567
|
// failures are branded as TransportErrors and count against the request
|
|
543
568
|
// deadline, exactly as an inline resolve would.
|
|
544
569
|
proxy = await resolveNativeProxy(options, clientOptions, warn, proxyAttemptOffset);
|
|
570
|
+
observeProxy?.(Boolean(proxy));
|
|
545
571
|
throwIfAmbientAborted(clientOptions.signal);
|
|
546
572
|
// For a registry allocator chain, skip an endpoint a prior attempt already
|
|
547
573
|
// tried rather than re-issuing the same request. Returning the sentinel
|
|
@@ -563,6 +589,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
563
589
|
requestInit.body = normalizeNativeFetchBody(options.body);
|
|
564
590
|
}
|
|
565
591
|
const response = await fetchWithHttpRedirectPolicy(requestUrl, requestInit, options.redirectPolicy);
|
|
592
|
+
observeProxy?.(Boolean(proxy), response.status);
|
|
566
593
|
const headers = Object.fromEntries(response.headers.entries());
|
|
567
594
|
if (statusRetryCodes && response.status >= 400) {
|
|
568
595
|
await drainNativeResponseBody(response);
|
|
@@ -596,7 +623,7 @@ async function fetchNativeHttp(baseUrl, url, method, options, clientOptions, war
|
|
|
596
623
|
clearTimeout(timeoutHandle);
|
|
597
624
|
}
|
|
598
625
|
}
|
|
599
|
-
async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn) {
|
|
626
|
+
async function fetchNativeHttpStream(baseUrl, url, method, options, clientOptions, warn, observeProxy) {
|
|
600
627
|
const serializedUrl = serializeHttpRequestUrl(baseUrl, url, options);
|
|
601
628
|
const { requestUrl } = serializedUrl;
|
|
602
629
|
const controller = options.timeout ? new AbortController() : undefined;
|
|
@@ -607,6 +634,7 @@ async function fetchNativeHttpStream(baseUrl, url, method, options, clientOption
|
|
|
607
634
|
try {
|
|
608
635
|
throwIfAmbientAborted(clientOptions.signal);
|
|
609
636
|
const proxy = await resolveNativeProxy(options, clientOptions, warn);
|
|
637
|
+
observeProxy?.(Boolean(proxy));
|
|
610
638
|
throwIfAmbientAborted(clientOptions.signal);
|
|
611
639
|
const requestInit = {
|
|
612
640
|
headers: options.headers,
|
|
@@ -676,164 +704,189 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
676
704
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
677
705
|
}
|
|
678
706
|
})();
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
707
|
+
const requestStarted = performance.now();
|
|
708
|
+
const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), retryOptions?.preset);
|
|
709
|
+
try {
|
|
710
|
+
return await runRequest();
|
|
711
|
+
}
|
|
712
|
+
finally {
|
|
713
|
+
telemetry.finish(performance.now() - requestStarted);
|
|
714
|
+
}
|
|
715
|
+
async function runRequest() {
|
|
716
|
+
const retryEnabled = Boolean(retryOptions &&
|
|
717
|
+
retryOptions.attempts > 1 &&
|
|
718
|
+
isProxyTransportRetryMethod(methodName, retryOptions));
|
|
719
|
+
const statusRetryEnabled = Boolean(retryEnabled &&
|
|
720
|
+
explicitRetry &&
|
|
721
|
+
retryOptions &&
|
|
722
|
+
retryOptions.statusCodes.length > 0 &&
|
|
723
|
+
headersOptions.throwOnHttpError !== false);
|
|
724
|
+
const attemptOptions = statusRetryEnabled
|
|
725
|
+
? { ...headersOptions, throwOnHttpError: false }
|
|
726
|
+
: headersOptions;
|
|
727
|
+
// Span the whole vendor chain on transport failures. Like ctx.stealth, a
|
|
728
|
+
// policy-managed proxy resolves a *different* endpoint/vendor per attempt
|
|
729
|
+
// (the flat proxyAttemptOffset rotates across the concatenated vendor pool
|
|
730
|
+
// spans), so a transport failure should advance to the next endpoint —
|
|
731
|
+
// potentially crossing into the fallback vendor — rather than stopping at
|
|
732
|
+
// the per-endpoint retry budget and stranding the request on the primary
|
|
733
|
+
// vendor. resolvePolicyTransportAttemptCap widens the cap to the chain span
|
|
734
|
+
// only for implicit, safe-method allocator requests; explicit retry
|
|
735
|
+
// policies (their documented `attempts` ceiling), unsafe methods, and
|
|
736
|
+
// static/non-registry vendors keep the retry budget. Status-code retries
|
|
737
|
+
// stay bounded by the retry budget regardless; only transport rotation gets
|
|
738
|
+
// the full span.
|
|
739
|
+
const policyProxy = (() => {
|
|
740
|
+
const policy = clientOptions.proxyPolicy ?? clientOptions.upstream?.proxy;
|
|
741
|
+
return policy && typeof policy === "object" ? policy : undefined;
|
|
742
|
+
})();
|
|
743
|
+
const usesPolicyAllocator = Boolean(policyProxy) && !options.proxy && !clientOptions.proxy;
|
|
744
|
+
const transportAttemptCap = retryOptions
|
|
745
|
+
? resolvePolicyTransportAttemptCap({
|
|
746
|
+
policy: policyProxy,
|
|
747
|
+
usesPolicyAllocator,
|
|
748
|
+
retryAttempts: retryOptions.attempts,
|
|
749
|
+
explicitRetry,
|
|
750
|
+
method: methodName,
|
|
751
|
+
})
|
|
752
|
+
: 1;
|
|
753
|
+
// Track resolved endpoints across a policy-allocator chain. Successive
|
|
754
|
+
// attempts rotate the flat offset across the concatenated vendor pool
|
|
755
|
+
// spans, but an under-filled allocation (fewer live endpoints than the
|
|
756
|
+
// configured pool size) makes the modulo mapping repeat endpoints before
|
|
757
|
+
// the offset reaches the next vendor. Rather than re-hammering an
|
|
758
|
+
// already-tried endpoint under backoff, fetchNativeHttp returns a skip
|
|
759
|
+
// sentinel for a duplicate; the loop then advances the flat offset without
|
|
760
|
+
// issuing the request, so it keeps walking toward — and into — the fallback
|
|
761
|
+
// vendor's pool span instead of stalling on the primary vendor.
|
|
762
|
+
// De-duplication is gated on the SAME predicate that widens the attempt cap
|
|
763
|
+
// (implicit, safe-method, registry-chain rotation). It must NOT engage for
|
|
764
|
+
// an explicit retry policy: there the caller's `attempts` count is the
|
|
765
|
+
// contract and each attempt must issue against whatever endpoint it resolves
|
|
766
|
+
// — even a repeat — instead of being silently skipped (which would collapse
|
|
767
|
+
// a `poolSize: 1` + `attempts: 3` request to a single fetch).
|
|
768
|
+
const dedupeAllocatorEndpoints = policyRotatesTransportVendorChain({
|
|
709
769
|
policy: policyProxy,
|
|
710
770
|
usesPolicyAllocator,
|
|
711
|
-
retryAttempts: retryOptions.attempts,
|
|
712
771
|
explicitRetry,
|
|
713
772
|
method: methodName,
|
|
714
|
-
})
|
|
715
|
-
:
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
code: "retry_exhausted",
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
if (isHttpStatusOutcome(outcome)) {
|
|
750
|
-
throw toUpstreamHttpError(outcome.status);
|
|
751
|
-
}
|
|
752
|
-
return outcome;
|
|
753
|
-
}
|
|
754
|
-
let lastError;
|
|
755
|
-
let lastErrorCode;
|
|
756
|
-
let lastStatus;
|
|
757
|
-
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
758
|
-
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
759
|
-
// increment it). Retry summaries and the status-retry budget must reflect
|
|
760
|
-
// issued requests, not the raw offset, so they stay accurate when partial
|
|
761
|
-
// allocations skip offsets.
|
|
762
|
-
let issued = 0;
|
|
763
|
-
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
764
|
-
throwIfAmbientAborted(clientOptions.signal);
|
|
765
|
-
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
766
|
-
// so the catch counts a thrown *transport* failure once without
|
|
767
|
-
// double-counting a status outcome that already incremented before it
|
|
768
|
-
// re-threw as an upstream HTTP error.
|
|
769
|
-
let issuedThisAttempt = false;
|
|
770
|
-
try {
|
|
771
|
-
const outcome = await executeOnce(attempt - 1);
|
|
773
|
+
});
|
|
774
|
+
const dedupeContext = dedupeAllocatorEndpoints ? { attempted: new Set() } : undefined;
|
|
775
|
+
const executeOnce = async (proxyAttemptOffset = 0) => {
|
|
776
|
+
const started = performance.now();
|
|
777
|
+
let proxyUsed = false;
|
|
778
|
+
let observedStatus;
|
|
779
|
+
const record = (event) => {
|
|
780
|
+
telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
|
|
781
|
+
};
|
|
782
|
+
try {
|
|
783
|
+
const outcome = await fetchNativeHttp(baseUrl, url, methodName, attemptOptions, clientOptions, warnOnce, statusRetryEnabled ? retryOptions?.statusCodes : undefined, proxyAttemptOffset, dedupeContext, (used, status) => {
|
|
784
|
+
proxyUsed = used;
|
|
785
|
+
observedStatus = status;
|
|
786
|
+
});
|
|
787
|
+
if (!isDedupeSkipOutcome(outcome))
|
|
788
|
+
record({
|
|
789
|
+
status: outcome.status,
|
|
790
|
+
...(isHttpStatusOutcome(outcome)
|
|
791
|
+
? { e: "upstream_http_error", statusRetry: true }
|
|
792
|
+
: {}),
|
|
793
|
+
});
|
|
794
|
+
return outcome;
|
|
795
|
+
}
|
|
796
|
+
catch (error) {
|
|
797
|
+
// Record before the retry loop can catch/rethrow a terminal status.
|
|
798
|
+
record(httpAttemptFailure(error, observedStatus));
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
if (!retryEnabled || !retryOptions) {
|
|
803
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
804
|
+
const outcome = await executeOnce();
|
|
772
805
|
if (isDedupeSkipOutcome(outcome)) {
|
|
773
|
-
//
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
|
|
806
|
+
// Single-shot path never de-duplicates (dedupeContext is undefined),
|
|
807
|
+
// but keep the union total.
|
|
808
|
+
throw new TransportError("HTTP request produced no terminal result", {
|
|
809
|
+
code: "retry_exhausted",
|
|
810
|
+
});
|
|
777
811
|
}
|
|
778
|
-
issued += 1;
|
|
779
|
-
issuedThisAttempt = true;
|
|
780
812
|
if (isHttpStatusOutcome(outcome)) {
|
|
781
|
-
lastStatus = outcome.status;
|
|
782
|
-
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
783
|
-
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), clientOptions.signal);
|
|
784
|
-
continue;
|
|
785
|
-
}
|
|
786
813
|
throw toUpstreamHttpError(outcome.status);
|
|
787
814
|
}
|
|
788
|
-
|
|
789
|
-
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
790
|
-
throw toUpstreamHttpError(response.status);
|
|
791
|
-
}
|
|
792
|
-
if (issued > 1) {
|
|
793
|
-
const summary = {
|
|
794
|
-
attempts: issued,
|
|
795
|
-
retries: issued - 1,
|
|
796
|
-
...(retryOptions.preset ? { preset: retryOptions.preset } : {}),
|
|
797
|
-
transport: "native",
|
|
798
|
-
...(lastErrorCode ? { lastErrorCode } : {}),
|
|
799
|
-
...(lastStatus ? { lastStatus } : {}),
|
|
800
|
-
};
|
|
801
|
-
clientOptions.onRetrySummary?.(summary);
|
|
802
|
-
}
|
|
803
|
-
return response;
|
|
815
|
+
return outcome;
|
|
804
816
|
}
|
|
805
|
-
|
|
817
|
+
let lastError;
|
|
818
|
+
// `attempt` walks the flat proxy offset across the full chain span; `issued`
|
|
819
|
+
// counts requests that were actually sent (skipped duplicate offsets do not
|
|
820
|
+
// increment it). Retry summaries and the status-retry budget must reflect
|
|
821
|
+
// issued requests, not the raw offset, so they stay accurate when partial
|
|
822
|
+
// allocations skip offsets.
|
|
823
|
+
let issued = 0;
|
|
824
|
+
for (let attempt = 1; attempt <= transportAttemptCap; attempt += 1) {
|
|
806
825
|
throwIfAmbientAborted(clientOptions.signal);
|
|
807
|
-
|
|
826
|
+
// Whether this offset actually issued a request (vs. a skipped duplicate),
|
|
827
|
+
// so the catch counts a thrown *transport* failure once without
|
|
828
|
+
// double-counting a status outcome that already incremented before it
|
|
829
|
+
// re-threw as an upstream HTTP error.
|
|
830
|
+
let issuedThisAttempt = false;
|
|
831
|
+
try {
|
|
832
|
+
const outcome = await executeOnce(attempt - 1);
|
|
833
|
+
if (isDedupeSkipOutcome(outcome)) {
|
|
834
|
+
// Duplicate endpoint from a partial allocation: advance the flat
|
|
835
|
+
// offset without issuing the request (no backoff, not a failure) so
|
|
836
|
+
// the loop keeps rotating toward the fallback vendor.
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
808
839
|
issued += 1;
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
|
|
840
|
+
issuedThisAttempt = true;
|
|
841
|
+
if (isHttpStatusOutcome(outcome)) {
|
|
842
|
+
if (outcome.retryable && issued < retryOptions.attempts) {
|
|
843
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt, outcome.headers), clientOptions.signal);
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
throw toUpstreamHttpError(outcome.status);
|
|
847
|
+
}
|
|
848
|
+
const response = outcome;
|
|
849
|
+
if (response.status >= 400 && headersOptions.throwOnHttpError !== false) {
|
|
850
|
+
throw toUpstreamHttpError(response.status);
|
|
851
|
+
}
|
|
852
|
+
if (issued > 1) {
|
|
853
|
+
const summary = telemetry.toTenantRetryPayload();
|
|
854
|
+
if (summary)
|
|
855
|
+
telemetry.observe(() => clientOptions.onRetrySummary?.(summary));
|
|
856
|
+
}
|
|
857
|
+
return response;
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
throwIfAmbientAborted(clientOptions.signal);
|
|
861
|
+
if (!issuedThisAttempt)
|
|
862
|
+
issued += 1;
|
|
863
|
+
lastError = error;
|
|
864
|
+
const proxyUsed = Boolean(error.proxyUsed);
|
|
865
|
+
if (attempt < transportAttemptCap &&
|
|
866
|
+
shouldRetryProxyTransportAttempt({
|
|
867
|
+
error,
|
|
868
|
+
explicitRetry,
|
|
869
|
+
method: methodName,
|
|
870
|
+
options: retryOptions,
|
|
871
|
+
proxyUsed,
|
|
872
|
+
})) {
|
|
873
|
+
await sleep(computeProxyTransportRetryDelayMs(retryOptions, attempt), clientOptions.signal);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
throw error;
|
|
823
877
|
}
|
|
824
|
-
throw error;
|
|
825
878
|
}
|
|
879
|
+
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
880
|
+
// e.g. the final offsets of a partial allocation all resolved to
|
|
881
|
+
// already-tried endpoints and were skipped. Surface the last real transport
|
|
882
|
+
// failure rather than a synthetic exhaustion error.
|
|
883
|
+
if (lastError !== undefined) {
|
|
884
|
+
throw lastError;
|
|
885
|
+
}
|
|
886
|
+
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
887
|
+
code: "retry_exhausted",
|
|
888
|
+
});
|
|
826
889
|
}
|
|
827
|
-
// Reached when the attempt cap is consumed without a terminal outcome —
|
|
828
|
-
// e.g. the final offsets of a partial allocation all resolved to
|
|
829
|
-
// already-tried endpoints and were skipped. Surface the last real transport
|
|
830
|
-
// failure rather than a synthetic exhaustion error.
|
|
831
|
-
if (lastError !== undefined) {
|
|
832
|
-
throw lastError;
|
|
833
|
-
}
|
|
834
|
-
throw new TransportError("HTTP retry exhausted without a terminal result", {
|
|
835
|
-
code: "retry_exhausted",
|
|
836
|
-
});
|
|
837
890
|
}
|
|
838
891
|
async function streamRequest(url, method, options = {}) {
|
|
839
892
|
const { headersOptions, methodName } = (() => {
|
|
@@ -855,7 +908,30 @@ export function createHttpClient(baseUrl, clientOptions = {}) {
|
|
|
855
908
|
throw redactSensitiveRequestError(error, url, options.sensitiveParams);
|
|
856
909
|
}
|
|
857
910
|
})();
|
|
858
|
-
|
|
911
|
+
// Streams never retry. Observe the header fetch without consuming its body.
|
|
912
|
+
const telemetry = startHttpTelemetry(scopedHttpTelemetry(clientOptions.httpTelemetry), "off");
|
|
913
|
+
const started = performance.now();
|
|
914
|
+
const alreadyAborted = clientOptions.signal?.aborted === true;
|
|
915
|
+
let proxyUsed = false;
|
|
916
|
+
const record = (event) => {
|
|
917
|
+
if (alreadyAborted)
|
|
918
|
+
return;
|
|
919
|
+
telemetry.recordAttempt({ ...event, ms: performance.now() - started, proxyUsed });
|
|
920
|
+
};
|
|
921
|
+
try {
|
|
922
|
+
const response = await fetchNativeHttpStream(baseUrl, url, methodName, headersOptions, clientOptions, warnOnce, (used) => {
|
|
923
|
+
proxyUsed = used;
|
|
924
|
+
});
|
|
925
|
+
record({ status: response.status });
|
|
926
|
+
return response;
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
record(httpAttemptFailure(error));
|
|
930
|
+
throw error;
|
|
931
|
+
}
|
|
932
|
+
finally {
|
|
933
|
+
telemetry.finish(performance.now() - started);
|
|
934
|
+
}
|
|
859
935
|
}
|
|
860
936
|
return {
|
|
861
937
|
request: async (url, options = {}) => request(url, options.method ?? "GET", options),
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Socket } from "node:net";
|
|
2
2
|
import { type TLSSocket } from "node:tls";
|
|
3
|
-
import { type ProxyProtocol } from "../config/loader.js";
|
|
3
|
+
import { type ProxyProtocol, type ProxyTelemetrySink } from "../config/loader.js";
|
|
4
4
|
import { type DynamicEgressRuleSnapshot } from "../native-egress-policy.js";
|
|
5
5
|
import type { NativeNetworkClient, NativeNetworkConnectInput, NativeNetworkConnection, NativeNetworkDynamicGrantOptions, NativeNetworkEgressGrant, NativeProviderConfig, NativeProxyEgressInfo, ProviderProxyPolicy, ProviderProxyProvider, EnvContext } from "../types.js";
|
|
6
|
+
import type { NativeTelemetrySink } from "./native-telemetry.js";
|
|
6
7
|
export { NativeEgressGrantExpiredError, NativeEgressNotDeclaredError, NativeIdleTimeoutError, NativeNetworkError, NativeProxyExpiredError, } from "./native-network-errors.js";
|
|
7
8
|
export type { NativeNetworkErrorCode } from "./native-network-errors.js";
|
|
8
9
|
export type NativeGatewayProxy = NativeProxyEgressInfo & {
|
|
@@ -15,6 +16,8 @@ export type NativeGatewayProxySynthesisInput = {
|
|
|
15
16
|
readonly now: number;
|
|
16
17
|
readonly protocol: ProxyProtocol;
|
|
17
18
|
readonly credentials: VendorCredentialResolver;
|
|
19
|
+
/** Existing proxy lease telemetry; adapters may report detailed allocation facts. */
|
|
20
|
+
readonly telemetry?: ProxyTelemetrySink;
|
|
18
21
|
};
|
|
19
22
|
export type VendorCredentialLookup = {
|
|
20
23
|
readonly kind: "present";
|
|
@@ -50,8 +53,14 @@ export type NativeGatewayProxyResolutionInput = {
|
|
|
50
53
|
readonly protocol?: ProxyProtocol;
|
|
51
54
|
readonly credentials?: VendorCredentialResolver;
|
|
52
55
|
readonly gatewaySynthesizers?: readonly NativeGatewayProxySynthesizer[];
|
|
56
|
+
readonly telemetry?: ProxyTelemetrySink;
|
|
57
|
+
readonly nativeTelemetry?: NativeTelemetrySink;
|
|
53
58
|
};
|
|
54
59
|
export type NativeNetworkClientOptions = {
|
|
60
|
+
/** Existing proxy lease contributor, shared with HTTP and stealth. */
|
|
61
|
+
readonly telemetry?: ProxyTelemetrySink;
|
|
62
|
+
/** Request-scoped connection, byte and lifecycle observations. */
|
|
63
|
+
readonly nativeTelemetry?: NativeTelemetrySink;
|
|
55
64
|
readonly proxyPolicy?: ProviderProxyPolicy;
|
|
56
65
|
readonly affinityKey?: string;
|
|
57
66
|
/** Stable credential/account identity; hashed before vendor synthesis. */
|