Files
purchases-ios/Sources/Networking/HTTPClient
Toni RicoandClaude Fable 5 8925524293 feat(remote-config): health-check-gated API source failover in HTTPClient (#7294)
* feat(remote-config): health-check-gated API source failover in HTTPClient

Wires the API source failover into the request pipeline, still behind the
usesRemoteConfigAPISources dangerous setting (internal and defaulting to
false, so released behavior is unchanged):

- Requests resolve their API source once (in start) and carry the handle,
  so a failure can report the exact source unhealthy and ETag/backoff
  retries keep targeting the same host.
- On a failure that points at the host, the source is health-checked
  before failing over: a healthy source surfaces the original error, an
  unhealthy one is reported and the request transparently retries on the
  next source (capped at 5 source attempts per request). The static
  per-endpoint fallback hosts stay the last resort.
- Unlike Android, device-connectivity errors (reusing
  NetworkError.isAllowedToRetryWithFallbackHost) skip the health check
  entirely and surface the original error: switching hosts can't fix a
  device that is offline.
- Both Backend HTTPClients share one APISourceFailover, so they walk one
  source list and share one health-check cache.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(remote-config): end-to-end API source failover integration tests

Port of purchases-android's BackendAPISourceFailoverIntegrationTest: real
Backend.getCustomerInfo through the production wiring (real HTTPClients,
shared APISourceFailover, real SourceHealthChecker probing over the URL
loading system) and a real RemoteConfigSourceProvider fed by an in-memory
sources topic, with OHHTTPStubs standing in for the source hosts and
recording exact per-host request order. Adds the iOS-only
device-connectivity scenario and a dangerous-setting-off wiring check on
top of Android's seven scenarios.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: model the request's API source as a single state enum

Replaces the three loosely coupled Request fields (apiSource,
apiSourceResolved, apiSourceAttemptCount) with one APISourceState enum
(unresolved / noSource / source(_, attemptCount:)), making inconsistent
combinations unrepresentable. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: adopt the narrowed APISourceFailover initializer

Passes just the usesRemoteConfigAPISources flag at the two construction
sites, following the signature change in the base branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: clarify why static fallback still runs when the source is healthy

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(remote-config): raise the API failover restart interval to 2 hours

The api source list now returns to the primary source 2 hours after it
first failed over instead of 10 minutes. Timer tests derive their time
values from the constant so future changes are single-line.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(remote-config): cover shared failover and probe-in-flight ordering

Addresses review feedback:
- Both HTTPClient lanes share one APISourceFailover: a failover on the
  main lane advances the source list the remote-config lane resolves.
- A request enqueued while a health check is in flight stays queued
  until the probe resolves and runs after the front-queued failover
  retry.
- Bind the source and attempt count in one pattern match at the
  failover guard.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor: drop the APISourceState.attemptCount accessor

Its only caller pattern-matches instead, surfacing the "must already
target a source" assumption at the one place it matters rather than
papering over impossible states with a 0 default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:25:33 +02:00
..