@apifuse/provider-sdk 2.2.0-beta.8 → 2.2.0-beta.9

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.
@@ -6,10 +6,11 @@ import type { ProxyResolutionOptions, ProxyVendorName } from "../config/loader.j
6
6
  import {
7
7
  DEFAULT_SMARTPROXY_POOL_SIZE,
8
8
  invalidateProxyResolutionCacheAsync,
9
+ policyResolvesRegistryVendorChain,
9
10
  ProxyResolutionError,
10
11
  resolvePolicyProxyPoolSpan,
12
+ resolvePolicyTransportAttemptCap,
11
13
  resolveProxyConfigAsync,
12
- SMARTPROXY_MAX_POOL_SIZE,
13
14
  vendorFromResolvedSource,
14
15
  } from "../config/loader.js";
15
16
  import { SDKError, TransportError } from "../errors.js";
@@ -53,11 +54,6 @@ const DEFAULT_PROFILE = "chrome-146";
53
54
  const MISSING_PROXY_WARNING =
54
55
  "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
55
56
 
56
- /**
57
- * Upper bound on attempts across a multi-vendor chain, so a two-vendor chain can
58
- * exhaust each vendor's pool before failing over and finally throwing.
59
- */
60
- const MAX_POLICY_PROXY_TOTAL_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE * 2;
61
57
  const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
62
58
  const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
63
59
  const PROXY_CONNECT_FAILURE_BODY_PATTERN =
@@ -682,14 +678,26 @@ function createSessionFetcher(
682
678
  (typeof clientOptions.upstream?.proxy === "object"
683
679
  ? clientOptions.upstream.proxy
684
680
  : undefined);
681
+ // The pool span is already bounded by each vendor's max pool size
682
+ // (smartproxy ≤20, nodemaven ≤50), so the configured span never exceeds
683
+ // the chain's true maximum — a large NodeMaven pool stays fully
684
+ // reachable rather than being truncated at an arbitrary ceiling.
685
685
  const policyProxyAttemptCap = Math.max(
686
686
  1,
687
- Math.min(
688
- MAX_POLICY_PROXY_TOTAL_ATTEMPTS,
689
- policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE,
690
- ),
687
+ policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE,
691
688
  );
692
- const maxAttempts = usesPolicyAllocator ? policyProxyAttemptCap : retryAttemptCap;
689
+ // A registry vendor chain (smartproxy/nodemaven) is the only policy whose
690
+ // successive attempts resolve a *different* endpoint, so it is the only one
691
+ // that may widen the attempt cap to the pool span, de-duplicate endpoints,
692
+ // and drive allocator stale-pool refresh. A static custom/decodo policy
693
+ // resolves the same URL every attempt: widening/refreshing it would resend
694
+ // the request dozens of times (up to maxAttempts × refreshes) and bypass
695
+ // retry:false and unsafe-method controls. Static policies therefore follow
696
+ // the ordinary transport-retry budget instead.
697
+ const rotatesRegistryChain =
698
+ usesPolicyAllocator && policyResolvesRegistryVendorChain(policyProxy);
699
+ const maxAttempts = rotatesRegistryChain ? policyProxyAttemptCap : retryAttemptCap;
700
+ const dedupeAllocatorEndpoints = rotatesRegistryChain;
693
701
  let lastError: unknown;
694
702
 
695
703
  for (
@@ -730,9 +738,14 @@ function createSessionFetcher(
730
738
  assertNoUnsupportedFingerprintOverrides(options);
731
739
  attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
732
740
  proxy = attemptProxy.url;
733
- if (proxy && usesPolicyAllocator) {
741
+ if (proxy && dedupeAllocatorEndpoints) {
742
+ // An under-filled allocation repeats endpoints (via the modulo
743
+ // pool mapping) before the flat offset crosses into the next
744
+ // vendor. Skip an already-tried endpoint and advance the offset
745
+ // rather than breaking — breaking here would strand the request on
746
+ // the primary vendor and never reach the fallback leg.
734
747
  if (attemptedProxies.has(proxy)) {
735
- break;
748
+ continue;
736
749
  }
737
750
  attemptedProxies.add(proxy);
738
751
  }
@@ -812,7 +825,7 @@ function createSessionFetcher(
812
825
  proxyAttemptStatus(normalizedError),
813
826
  );
814
827
  lastError = normalizedError;
815
- if (proxy && usesPolicyAllocator && isProxyPoolRefreshableError(normalizedError)) {
828
+ if (proxy && rotatesRegistryChain && isProxyPoolRefreshableError(normalizedError)) {
816
829
  stalePoolError = normalizedError;
817
830
  if (shouldRunProxyAuthDiagnostic(normalizedError)) {
818
831
  stalePoolDiagnosticProxy = proxy;
@@ -822,11 +835,28 @@ function createSessionFetcher(
822
835
  }
823
836
  break;
824
837
  }
838
+ // Cap the number of transport retries. For a policy-allocator chain,
839
+ // every attempt resolves a *different* endpoint/vendor (poolIndex
840
+ // rotates across the concatenated vendor pool spans), so a transport
841
+ // failure is a signal to advance to the next endpoint — potentially
842
+ // crossing into the fallback vendor — not to retry the same endpoint.
843
+ // Truncating that rotation at the per-endpoint retry budget would
844
+ // strand the request on the primary vendor and never reach the
845
+ // fallback, since the crossover only happens once the flat attempt
846
+ // index exceeds the primary vendor's pool size (~10-20).
847
+ // resolvePolicyTransportAttemptCap widens to the full chain span only
848
+ // for implicit, safe-method allocator requests; explicit retry
849
+ // policies (their documented `attempts` ceiling), unsafe methods, and
850
+ // static/non-registry vendors keep the per-endpoint retry budget.
851
+ const transportRetryCap = resolvePolicyTransportAttemptCap({
852
+ policy: policyProxy,
853
+ usesPolicyAllocator,
854
+ retryAttempts: stealthRetryOptions?.attempts ?? 1,
855
+ explicitRetry: hasExplicitRetryPolicy,
856
+ method,
857
+ });
825
858
  if (
826
- attempt + 1 <
827
- (stealthRetryOptions
828
- ? Math.min(maxAttempts, stealthRetryOptions.attempts)
829
- : maxAttempts) &&
859
+ attempt + 1 < transportRetryCap &&
830
860
  shouldRetryProxyTransportAttempt({
831
861
  error: normalizedError,
832
862
  explicitRetry: hasExplicitRetryPolicy,
@@ -845,7 +875,7 @@ function createSessionFetcher(
845
875
  }
846
876
 
847
877
  if (
848
- usesPolicyAllocator &&
878
+ rotatesRegistryChain &&
849
879
  stalePoolError &&
850
880
  refreshAttempt < MAX_POLICY_PROXY_POOL_REFRESHES
851
881
  ) {
@@ -512,11 +512,22 @@ function toErrorResponse(error: unknown, requestId?: string): OperationErrorResp
512
512
  };
513
513
  }
514
514
 
515
+ // A masked internal error MUST NOT be advertised as retryable: without an
516
+ // explicit retryable:false the hub (bori provider-backed engine) defaults 5xx
517
+ // to retryable:true, which turns a deterministic pre-upstream crash into an
518
+ // infinite START->CONTINUE->restart loop (2026-07-22 catchtable reserve RCA).
519
+ // We still refuse to leak message/stack — only the error class name (or the
520
+ // primitive type for non-Error throwables) is surfaced for ops triage.
515
521
  return {
516
522
  error: {
517
523
  code: "internal_error",
518
524
  message: "Internal error",
519
525
  ...(requestId ? { requestId } : {}),
526
+ details: {
527
+ retryable: false,
528
+ category: "internal_error",
529
+ errorClass: error instanceof Error ? error.name : typeof error,
530
+ },
520
531
  },
521
532
  };
522
533
  }