Commit Graph
15 Commits
Author SHA1 Message Date
Dave DeLong 6ed515865a Simplify HTTP path types (#7987) 2026-10-23 11:11:52 -06:00
e7384b8282 Re-tier HTTP request timeouts around per-host memory (#7179)
* Re-tier HTTP request timeouts around per-source memory

Replaces the single global "recent timeout" slot in `HTTPRequestTimeoutManager`
with a per-host map (`host -> lastTimeoutDate`, 10-minute per-entry expiry,
pruned on access) and re-tiers the request timeouts:

- Main-source, endpoint with no fallback-URL support: 15s base / 5s reduced
- Main-source, endpoint with fallback-URL support: 5s base / 2s reduced
- Fallback-host and proxied requests: flat 30s (never consult the memory)

A main-source request to host H uses the reduced tier iff H has a non-expired
timeout entry. Timeouts are now recorded for any non-fallback request (dropping
the previous fallback-support-only condition), keyed by attempt host; a
successful main-source response clears only that host's entry. Renames the
`timeoutOnMainBackendForFallbackSupportedEndpoint` result to `mainSourceTimedOut`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Define flat timeout tier as a Timeout constant

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add exhaustive multi-host tests for timeout manager

Co-authored-by: Cursor <cursoragent@cursor.com>

* Honor developer networkTimeout via internal NetworkTimeout enum

Introduce an internal NetworkTimeout enum (.default/.custom) threaded from
Configuration down to HTTPRequestTimeoutManager. When a developer sets
networkTimeout, it replaces the built-in base/flat tiers while the reduced
fail-fast tiers stay fixed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add NetworkTimeout.swift to RevenueCat.xcodeproj

The file was only picked up by the SPM/Tuist directory globs, so the
hand-maintained Xcode project build failed to resolve the type.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Gate re-tiered no-fallback timeouts behind usesRemoteConfigAPISources

The aggressive no-fallback fail-fast timeouts and their per-host timeout
recording now apply to main-API requests only when the usesRemoteConfigAPISources
dangerous setting is enabled. With it disabled (the default) the legacy network
timeout is preserved. The gating is a per-call timeout(...) parameter so
blob-source downloads can opt into the re-tiered timeouts independently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep the legacy flat timeout when re-tiered timeouts are off, and share one manager

The fallback-host and proxied tiers returned the new 30s flat timeout regardless of
`usesRemoteConfigAPISources`, halving the legacy 60s (or custom) network timeout for
proxy users and every fallback-host request. They now honor `reTieredTimeoutsEnabled`
like the no-fallback tier does, so only the fallback-supported tiers behave the same
either way.

`Backend` also built one timeout manager per HTTPClient, so the main and remote-config
lanes kept independent per-host memories despite talking to the same hosts. They now
share one, and `HTTPClient` requires both the manager and the network timeout so this
wiring can't be dropped silently.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Clear a host's fail-fast memory as soon as it answers

The memory exists to remember how quickly a host responds, so a non-error
response is evidence the host recovered even when decoding or verifying it
fails afterwards. Deciding off the raw status code instead of the parsed result
keeps those failures from leaving a responsive host stuck on the reduced
timeout tier.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Name the requestTimeoutManager in the fail-fast comment

Co-authored-by: Cursor <cursoragent@cursor.com>

* Tighten comments on the Backend-shared failover and timeout manager

Name the instances the comments describe so it's clear what is shared.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Record API source timeouts before health checks

* Test shared timeout state across backend lanes

* Normalize URL timeout classification across platforms

* Remove unused RemoteConfigAPI test hook

* Skip HTTP stubs lane tests on watchOS

* Treat the remote config fallback path as a fallback host request

`FallbackPath` bakes the fallback host into its `serverHostURL`, but `fallbackUrlIndex` is
nil on those requests, so `isFallbackURLRequest` was false and they were classified as
main-source: with API sources enabled they drew the aggressive 15s/5s no-fallback tiers
instead of the flat fallback timeout, their timeouts armed the per-host fail-fast memory
for the fallback host, and their successes cleared the main source's entry.

Add `isFallbackHostPath` so a path can declare that it targets a fallback host on its own,
and use it alongside the index when picking the tier and updating the memory.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Rick van der Linden <rick.vanderlinden@revenuecat.com>
2026-08-03 14:51:50 +02:00
Rick b82223e5a1 feat(remote-config): add static fallback config endpoint (#7182)
* feat(remote-config): add static fallback config endpoint

* refactor(remote-config): clarify static fallback path name

* test(remote-config): cover static fallback signature failures

* fix(remote-config): use static fallback only without cache

* test(remote-config): cover static fallback external blobs

* refactor(remote-config): align static fallback naming

* refactor(remote-config): scope static fallback path

* fix(remote-config): mark fallback no content fresh

* fix(remote-config): align fallback accept headers

* refactor(remote-config): rename fallback endpoint types

* fix(remote-config): enable etags for fallback config

* fix(remote-config): skip fallback when proxy is configured

* fix(remote-config): rely on etag cache for fallback

* refactor(remote-config): reuse fallback retry eligibility

* fix(remote-config): disable after fallback client errors
2026-07-10 14:17:31 +02:00
Antonio PallaresandCursor cbdc1ae216 Don't switch hosts on device-connectivity errors (#7176)
* other(networking): don't switch hosts on device-connectivity errors

Add `NetworkError.isDeviceConnectivityError` (true only for device-side
URLError codes) and narrow the fallback-host retry gate to
`isTransient && !isDeviceConnectivityError`, so a device that is offline
no longer triggers pointless host switching. Host-side and ambiguous
failures stay switch-eligible.

Co-authored-by: Cursor <cursoragent@cursor.com>

* other(networking): make isDeviceConnectivityError doc less usage-specific

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-10 12:06:50 +02:00
d4f0fb2686 refactor(ads): reuse SDK retry classification for poll errors (#7009)
* refactor(ads): move rewardverification to coresdk

* refactor(ads): move rewardverification to coresdk

* refactor(ads): move rewardverification to coresdk

* refactor(ads): remove unnecessary comment

* fix(ads): linter and behavior fixes

* fix(ads): gate reward-verification VC cache invalidation out of CustomEntitlementComputation

* refactor(ads): rename reward-verification transactionId → clientTransactionID

* fix(ads): remove duplicated SSV payload logic from AdMob adapter

* chore(ads): cover install via injectable

* chore(ads): fix linter

* refactor(ads): introduce RewardVerificationToken

* refactor(ads): add reward_verification_completed log

* Update AdapterSDKs/RevenueCatAdMob/Tests/RevenueCatAdMobTests/PresentRewardVerificationTests.swift

Co-authored-by: Antonio Pallares <ajpallares@users.noreply.github.com>

* fix(ads): linter

* fix(ads): key reward verification poll retries on HTTP status

Retry network/offline transport errors and any HTTP 5xx during reward
verification polling, keying on the HTTP status code rather than the
mapped ErrorCode. Empty/unparseable 5xx responses are now retried, and
4xx client errors fail fast as .backendError instead of being retried to
a misleading .timeout.

* refactor(ads): drop redundant comments on poll retry classifier

* refactor(ads): read poll HTTP status via typed NSError accessor

* refactor(ads): reuse SDK retry classification for poll errors

Classify reward-verification poll retries from the structured BackendError
instead of re-deriving the HTTP status from a flattened public NSError.

Adds a reusable NetworkError.isTransient (5xx and connection-level failures
are transient, 4xx are terminal) and forwards it from BackendError; the
HTTPClient fallback-host retry check now delegates to it. The poll path keeps
the structured BackendError via an internal fetchRewardVerificationStatus,
letting the poller reuse BackendError.isTransient and dropping the bespoke
NSError.httpStatusCode accessor.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(ads): switch from experimental to internal

* docs(ads): reference BackendError in Poller doc comment

---------

Co-authored-by: Peter Porfy <peter.porfy@revenuecat.com>
Co-authored-by: Pol Miro <polmiro@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-18 03:45:52 -06:00
f98f57f8e9 fix(workflows): Don't serve stale workflows when the backend rejects the request (#6946)
* fix(workflows): don't restore stale workflows from disk on a 4xx

The getWorkflowsList failure branch restored the cached list + details
on any error. That's right for transient errors (5xx / offline) but
wrong for a 4xx, where the backend is authoritatively rejecting the
request (workflows disabled, unauthorized), so we shouldn't serve stale
prefetched data.

Generalize the existing offerings classification into
BackendError/NetworkError.shouldFallBackToCache (the offerings property
now delegates to it, no behavior change) and gate the workflows disk
restore on it. 4xx skips the restore; 5xx and connection failures still
restore.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(workflows): fill gaps found in code review of 4xx stale-restore fix (#6948)

- testGetWorkflowsListCallsOnCompleteOnClientError: verifies onComplete()
  fires on 4xx even though disk restore is skipped (callers must not block)
- testGetWorkflowsListRestoresDetailsFromDiskOnServerError: verifies that
  on 5xx both list mapping AND workflow details are restored from disk, not
  just the list mapping

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: drop redundant shouldFallBackToCachedOfferings alias

Per review: shouldFallBackToCachedOfferings was only a thin delegate to
shouldFallBackToCache after the prior refactor. Remove it on NetworkError
and BackendError and point callers (OfferingsManager, tests) at
shouldFallBackToCache directly. Behavior-preserving.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cesar de la Vega <664544+vegaro@users.noreply.github.com>
2026-06-08 19:41:52 +00:00
d11c054407 Use cached offerings on network errors (#5707)
* Use offerings cache on (most) network errors

* Add unit tests for `NetworkError`'s `shouldFallBackToCachedOfferings`

* Add unit tests for `shouldFallBackToCachedOfferings`

* Fix lint

* Add some more unit tests

* Add `isLoadShedderResponse` and `isFallbackURLResponse` properties to `VerifiedHTTPResponse`

Keeps the values also when returned by the ETagManager

* lint

* Add some tests for ETagManager

* Rename `isFallbackURLResponse` to `isFallbackUrlResponse` to prevent decoding issues

* Add tests for actual source of original cached response of ETagManager

* Add tests to HTTPClientTests regarding the new booleans

* lint

* Add `originalSource` to `CustomerInfo`

* Adds `isLoadedFromCache` boolean to CustomerInfo

* Add docs

* Unit tests for new customer info properties

* Make `OriginalSource.CustomerInfo` a `String` enum

* Add decoding tests for Customer Info with new original source field

* Add `.loadShedder` check for Customer Info obtained in Load Shedder integration tests

* Update fixture

* improve internal docs

* Add intermediate object `Offerings.Contents` to wrap `OfferingsResponse` + `OriginalSource` and `loadedFromCache`

* Adapt existing unit tests to changes in `Offerings` underlying objects

* Check Offerings original source in existing integration tests

* Fix StoreKit unit tests

* Fix RevenueCatUI tests

* More Offerings.Contents tests

* Fix test

* Add more tests

* Fix lint

* [skip ci] Generating new test snapshots (#5741)

* [skip ci] Generating new test snapshots (#5742)

* [skip ci] Generating new test snapshots (#5743)

* [skip ci] Generating new test snapshots (#5744)

* [skip ci] Generating new test snapshots (#5745)

* [skip ci] Generating new test snapshots (#5746)

* [skip ci] Generating new test snapshots (#5747)

* [skip ci] Generating new test snapshots (#5748)

* Use enum for `HTTPResponseSource`

* Update tests

* Adapt to changes in `VerifiedHTTPResponse`

* Lint

* Adapt to changes in `VerifiedHTTPResponse`

* update tests

* Add log when a request to the fallback URL was handled by load shedder (which should never happen)

* rename `HTTPResponseSource` to `HTTPResponseOriginalSource`

* Adapt to changes

* fix lint

* Adapt to changes

* Fix CustomerInfo's OriginalSource implementation + fix unit test

* Fix tests compilation

* Add Cached offerings integration test

* Fix RCUI compilation

* Move isFromCache boolean from `Offerings.Contents` to `Offerings`

* Expand `ForceServerErrorStrategy` functionality

* Improve log when faking error response

* Correct implementation of fake error response without making a request (ForceServerErrorStrategy)

* Update tests + more tests

* Fix compilation of `OfferingsManagerTests`

* Fix compilation of `DebugViewSwiftUITests`

* Improve tests

* Fix `OfflineWithNoMappingStoreKitIntegrationTests`

* FIx compilation

---------

Co-authored-by: Toni Rico <toni.rico.diez@revenuecat.com>
Co-authored-by: RevenueCat Git Bot <72824662+RCGitBot@users.noreply.github.com>
2025-11-07 10:22:16 +00:00
NachoSoto ea952f0108 ErrorUtils.purchasesError(withSKError:): handle URLErrors (#3346)
Fixes #3343.

`SKPaymentTransaction.error` docs say:
> The error property is undefined except when `transactionState` is set
to `SKPaymentTransactionState.failed`. Your application can read the
error property to determine why the transaction failed. For a list of
error constants, see `SKErrorDomain` in StoreKit Constants.

Which is why [`PurchasesOrchestrator` only considered that
domain](https://github.com/RevenueCat/purchases-ios/blob/4.28.1/Sources/Purchasing/Purchases/PurchasesOrchestrator.swift#L792).

This expands this handling to consider any other network error,
therefore providing more precise errors.

Since offline connection error detection was only in `HTTPClient`, I
also moved that out and removed `NetworkError.offlineConnection`, so
that we detect this when converting `NetworkError.networkError` instead.
2023-10-25 20:25:09 -07:00
NachoSoto 833524314d HTTPClient: extracted HTTPRequestPath protocol (#2986)
This paves the way to add a second set of paths with a separate server
URL to be able to shed requests to offerings and product entitlement
mapping to a separate server.

### Changes:
- New `HTTPRequestPath` with the same properties as `HTTPRequest.Path` +
a new `serverHostURL`
- Moved proxy logic from `SystemInfo`/`HTTPClient` to `HTTPRequestPath`
- Simplified `HTTPRequest` implementation
2023-08-10 15:23:13 -07:00
NachoSoto 6d1cf97a41 ErrorResponse: drastically improved error messages, no more "unknown error"s (#2660)
## Description:

We've gotten several reports with error messages that aren't very
helpful, like #2390.

Turns out that whenever the API returned a non-successful response but
without an error code, we always produced _"unknown error"_.
This improves the behavior to default to showing the status code. One
example that was very obvious is setting up the SDK with an invalid API
key.

## Examples:

### `OfferingsManager.Error`:

- Before:
```
[Purchases] - ERROR: 🍎‼️ Error fetching offerings - The operation couldn’t be completed. (RevenueCat.OfferingsManager.Error error 0.)
Underlying error: The operation couldn’t be completed. (RevenueCat.NetworkError error 6.)
```

- After:
```
[Purchases] - ERROR: 🍎‼️ Error fetching offerings - The operation couldn’t be completed. (RevenueCat.OfferingsManager.Error error 0.)
Underlying error: Unknown error. API request failed with status code 401
```

### `BackendError`:
- Before:
```
[Purchases] - ERROR: 😿‼️ Unknown error.
```

- After:
```
[Purchases] - ERROR: 😿‼️ Unknown error. API request failed with status code 401
```

### Request debug logs:
- Before:
```
[Purchases] - DEBUG: ℹ️ API request failed: GET /v1/subscribers/$RCAnonymousID:988f9b85609b4a57af255750bec1fbb2/offerings: Unknown error.
```
- After:
```
[Purchases] - DEBUG: ℹ️ API request failed: GET /v1/subscribers/$RCAnonymousID:988f9b85609b4a57af255750bec1fbb2/offerings: Unknown error. API request failed with status code 401
```

### `CustomerInfoManager`:
- Before:
```
[Purchases] - WARN: ⚠️ Attempt to update CustomerInfo from network failed.
The operation couldn’t be completed. (RevenueCat.BackendError error 0.)
Underlying error: Unknown error
```

- After:
```
[Purchases] - WARN: ⚠️ Attempt to update CustomerInfo from network failed.
The operation couldn’t be completed. (RevenueCat.BackendError error 0.)
Underlying error: Unknown error. API request failed with status code 401
```
2023-06-16 09:19:16 -07:00
NachoSoto 6986a2f95e NetworkError.signatureVerificationFailed: added status code to error userInfo (#2657) 2023-06-16 07:55:21 -07:00
NachoSoto a6623640e8 Implemented NetworkError.isServerDown (#2367) 2023-03-23 08:15:57 -07:00
NachoSoto f05aabd176 HTTPClient: added signature validation and introduced ErrorCode.signatureVerificationFailed (#2272)
Fixes [CSDK-634].

## Changes:
- Created `HTTPResponseValidationResult`
- Added `HTTPResponseValidationResult` and `HTTPClient.ResponseHeaders`
to `HTTPResponse`
- Added new `ErrorCode.signatureVerificationFailed`
- Extracted `SigningType` to create `MockSigning` to test `HTTPClient`
without bothering with signatures
- Refactored `ETagManager` to use `HTTPResponse`, making it easier to
pass through response headers and verification results
- Stored `HTTPResponseValidationResult` in `ETagManager` cache
- `HTTPClient` handles signatures from response based on
`SystemInfo.responseVerificationLevel`
- Renamed `HTTPRequest.createIntegrityEnforcedRequestRequest` to
`HTTPRequest.createWithResponseVerification`
- Added lots of tests for this new behavior in `HTTPClient`
- Added lots of new tests for `ETagManager`

[CSDK-634]:
https://revenuecats.atlassian.net/browse/CSDK-634?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
2023-02-16 03:06:59 -08:00
NachoSoto d4ea390970 Introduced type-safe PurchasesError errors and fixed some incorrect returned error types (#1879)
This is the last change from what was started with #1437. That PR introduced a few private error types, but for the errors returned in the public `APIs` we were still passing around `Error`s everywhere, which didn't provide any compile-time guarantees.

### Goals:
- Ensure that public APIs *only* return `ErrorCode`s wrapped with the additional `userInfo`, and not accidentally return other types like `SKError`, `BackendError`, `ErrorCode` directly, etc.
- Guarantee type-safety when passing errors around within our internal implementations instead of assuming certain thrown errors are of a particular type.

### Changes:
- Created `PurchasesError` for wrapping an `ErrorCode` with the additional `userInfo`.
- Changed returned errors from `Error` to `NSError`. This is a change in the API, but the new type is a more specific `Error` so it shouldn't lead to any changes in client apps.
- Because `PurchasesError` isn't implicitly convertible to `NSError`, this ensures that we don't return those values directly, and instead use `PurchasesError.asPublicError`.
- Changed `ErrorCodeConvertible` to a now more precise `PurchasesErrorConvertible`.

Thanks to this new type-safety this PR fixes at least 3 bugs where we were returning private errors instead of `ErrorCode`s.

### Testing:

Thanks to #1871 we know that the returned errors are still convertible to `ErrorCode` so users can `switch` over them.
2022-09-05 13:46:07 -07:00
Joshua Liebowitz 858bef3f9b Extract Offerings-related things into OfferingsAPI (#1706)
Extract Offerings-related things into OfferingsAPI
2022-06-16 14:00:40 -07:00