Skip to content

Ship opt-in transport retries and per-request controls for v1 #92

Open

Description

@betegon

Goal

Required for v1.0.0 (#93): ship a small, opt-in transport retry helper with per-request controls that work across the SDK and consumer adapters. Keep the default REST request path single-attempt, and preserve consumer ownership of credentials and response caches.

This resolves the scope question from #81: share reusable retry mechanics in @sentry/api; consumers supply their authentication, instrumentation, and application-specific policies. Progressive-accuracy queries remain a separate capability.

Why this belongs in v1

CLI, MCP, and frontend consumers currently implement their own retry behavior. MCP #1283 supplies prior art for bounded GET retries on transient gateway errors. CLI #1649 demonstrates why retries and cache behavior also need per-call controls for the issue link/unlink flow (#1559):

  • A Sentry App link calls the external provider before storing the association in Sentry. A failure after the provider acted can leave nothing stored to deduplicate a subsequent POST. expectedExternalIssueUrl protects an already-stored association; it does not guarantee exactly-once callbacks. This mutation must not be replayed automatically.
  • Preflight reads must use current associations. A stale cached response can make unlink incorrectly report that an association is already absent. These reads need to bypass both cache lookup and storage without changing defaults for other calls.

Today CLI #1649 captures these controls inside its custom fetch. It does not pass a retry option to the SDK, and its local cache checks the wrapper's options rather than the incoming Request.cache. Adding SDK retries above that adapter, or only forwarding the standard cache option, would not preserve the intended guarantees. Adoption must exercise the full composed request path.

Toolkit #1418 adds a complementary cache-refresh scenario: refreshing the CLI's organization inventory must also bypass a stale HTTP response cache. It passes cache: "reload" on each fetched page so a fresh, cacheable response can replace the old entry. Unlike no-store, reload bypasses cache reads while retaining writes under the consumer's existing cacheability rules. The PR also needs a local type intersection to pass this option through generated SDK calls in its Node type environment. Organization selection, configuration precedence, aliases, cross-organization search, and persistent inventory storage remain CLI responsibilities.

Required contract

  • Export an opt-in retry helper that composes with an injected transport. Initially retry only GET/HEAD on 502/503/504; mutations must not become retryable implicitly. Calls that do not opt in retain the existing single-attempt behavior.
  • Provide bounded retry counts and backoff, with documented defaults. Preserve method, body, headers, and cancellation. Abort must stop in-flight work and pending backoff without scheduling another attempt.
  • Define typed per-call overrides and their precedence over client defaults. Overrides must be isolated across concurrent calls and must not mutate process-wide or client-wide settings. Settle the exported helper/options shape before the v1 API freeze; the earlier withRetry sketch was not a frozen API.
  • Provide an explicit no-replay control (retry: false in CLI #1649). For a dispatched resource request it must allow at most one attempt across the SDK and the supported adapter composition, including suppressing reactive 401 refresh-and-replay. Proactive credential refresh before dispatch remains consumer-owned and is distinct from replaying the resource request. Document the adapter contract; the SDK cannot disable retries hidden inside an arbitrary custom transport.
  • Make retry ownership explicit during adoption. Replace or disable the equivalent consumer retry loop when enabling the shared helper; do not multiply budgets by nesting loops. Retained authentication or application-level retry policies must compose deliberately and honor the no-replay control, including re-execution by query frameworks. Pagination and progressive accuracy remain distinct domain mechanisms, not additional transport retry budgets.
  • Preserve the standard per-call cache: "no-store" option through the SDK and adapters. Consumer-owned response caches must honor it by skipping both lookup and storage for that request. It does not flush existing entries or replace mutation invalidation. Keep persistent storage, TTL policy, cache invalidation, and CLI --fresh behavior with consumers; this work does not introduce an SDK response cache.
  • Expose typed per-call cache: "reload" alongside cache: "no-store" in supported runtime type environments, including Node and Bun, without consumer-side type intersections or casts. Preserve the option through standalone and bound generated operations, routing, pagination, and injected fetch adapters. For reload, supported consumer cache adapters must skip cache lookup and allow fresh, cacheable responses to update storage according to their existing policy. Keep no-store semantics distinct: skip both lookup and storage. Neither mode changes subsequent calls' defaults or introduces an SDK-owned response cache. These request hints do not invalidate organization inventory or routing/discovery caches.
  • Preserve the actual final HTTP response or transport failure through the existing error contract. In particular, a final-attempt 401 must remain a 401 instead of becoming an internal retry-exhaustion error. Credential storage/refresh, login guidance, UI messages, and CLI exit codes remain consumer responsibilities.

Validation and adoption evidence

  • SDK contract tests cover eligible GET/HEAD failures and budgets, mutation non-replay, per-call disabling, concurrent override isolation, cancellation during fetch/backoff, and final-response/error preservation.
  • Test cache-option propagation through both Request and (input, init) adapter forms. Seed a stale consumer cache and verify that a no-store preflight fetches fresh data without writing it back or changing subsequent calls' defaults.
  • Exercise the Toolkit #1418 refresh scenario through both Request and (input, init) adapters: seed stale HTTP responses, verify that every fetched pagination page preserves reload and bypasses adapter cache reads, cacheable network responses replace stale entries, and subsequent ordinary reads use the refreshed cache. This does not guarantee freshness beyond the controlled adapter cache. Compare with no-store, which must leave existing entries unchanged and write no new entries. Verify per-call isolation and compile both modes against the public package in supported Node/Bun consumers without local type workarounds. This validates transport composition; the decision to refresh or search other organizations remains consumer-owned.
  • Demonstrate an integrated CLI preflight and Sentry App mutation through generated SDK operations. Simulate a provider effect followed by HTTP failure, timeout, or connection loss and verify that the client does not replay the mutation. Include reactive 401 handling and confirm that no second SDK/consumer retry loop remains. Reuse the behavior introduced by CLI #1649; that PR alone is not SDK adoption evidence.
  • Compile and exercise the public package helper in a clean consumer with an injected transport, and document how existing CLI/MCP/frontend retry policies compose or are replaced. Link implementation and consumer validation from v1.0.0 #93, distinguishing mocked contract checks from real deployment verification. A full rewrite of every consumer is not required.

Remaining design decisions

  • Exact public helper/options shape and how supported adapters receive per-call retry policy without encoding it as an HTTP header.
  • Default budget, delays, and jitter.
  • Whether selected network failures are included in v1. Cancellation must never be retried. Broader 500, 408, or 429/Retry-After policies are outside the minimum GET/HEAD gateway-retry contract and must not be inherited accidentally from the CLI implementation.

Frontend integration boundary

For #103, preserve the frontend's existing TanStack retry policy and real RequestError semantics by default. A custom error adapter must not cause previously excluded 4xx responses to be retried. Enable SDK retries only with an explicit ownership decision; test the total attempts across TanStack, SDK, and retained auth/sudo behavior. Do not infer that all 4xx/5xx failures share one recovery policy.

#102 only adds inferred API contracts to existing apiOptions; it does not replace the transport or add another retry layer.

Activity

  1. changed the title [-]Ship an opt-in retry/backoff for transient upstream 5xx (502/503/504)[/-] [+]Ship opt-in transport retries and per-request controls for v1[/+] on Sep 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions