Skip to content

fix(networking): skip ETag cache for responses exceeding 4 MB to prevent OOM on large /offerings payloads#3658

Open
tsushanth wants to merge 1 commit into
RevenueCat:mainfrom
tsushanth:fix-etag-oom-large-offerings
Open

fix(networking): skip ETag cache for responses exceeding 4 MB to prevent OOM on large /offerings payloads#3658
tsushanth wants to merge 1 commit into
RevenueCat:mainfrom
tsushanth:fix-etag-oom-large-offerings

Conversation

@tsushanth

@tsushanth tsushanth commented Jun 26, 2026

Copy link
Copy Markdown

Problem

ETagManager.storeResult calls HTTPResultWithETag.serialize(), which internally calls JSONObject.toString() to build the entire response body as a single in-memory String. For large /offerings responses — for example those containing multiple published V2 paywalls — this single contiguous allocation can reach tens of MB and throw OutOfMemoryError on devices with a small Java heap cap (commonly 128 MB on low-end Android 10–12 hardware).

The crash site from production:

java.lang.OutOfMemoryError: Failed to allocate a 37748744 byte allocation with 8388608 free bytes
    at java.util.Arrays.copyOf
    at java.lang.AbstractStringBuilder.ensureCapacityInternal
    ...
    at org.json.JSONObject.toString
    at com.revenuecat.purchases.common.networking.HTTPResultWithETag.serialize(ETagManager.kt:33)
    at com.revenuecat.purchases.common.networking.ETagManager.storeResult(ETagManager.kt:149)

The reporter observed 600+ fatal events over 8 days, spiking sharply after publishing 4 offerings with V2 paywalls. Removing those paywalls stopped the crashes immediately.

Fix

Add a payload-size guard at the top of storeResult. If result.payloadText.length exceeds MAX_CACHEABLE_PAYLOAD_BYTES (4 MB), the response is returned to the caller normally but skipped for caching, preventing the large allocation. A WARNING log is emitted so operators can detect abnormally large payloads. Normal-sized responses are unaffected.

The 4 MB threshold is generous for typical /offerings responses while well below the observed crash floor (~37 MB). It can be adjusted upward later if needed.

MAX_CACHEABLE_PAYLOAD_BYTES is internal and therefore directly accessible from the existing ETagManagerTest for unit-test coverage.

Trade-offs

When a response is larger than the threshold the ETag short-circuit is unavailable for that URL: the next request will always go to the network and re-download the full response instead of receiving a 304 Not Modified. This is the correct safety/correctness trade-off — a cache miss is always recoverable, an OOM crash is not. The underlying /offerings response size issue is separate and can be addressed by the backend team independently.

Closes #3628


Note

Medium Risk
Touches core networking cache behavior for large responses; behavior change is intentional (skip cache vs crash) with limited blast radius for typical payloads under 4 MB.

Overview
ETagManager.storeResult now checks payloadText length before persisting. Responses over MAX_CACHEABLE_PAYLOAD_BYTES (4 MB) are still returned to callers but are not written to SharedPreferences, avoiding the large JSONObject.toString() allocation during HTTPResultWithETag.serialize that was causing production OOMs on huge /offerings payloads.

Oversized skips emit a WARNING via the new NetworkStrings.ETAG_RESPONSE_TOO_LARGE_TO_CACHE message. Trade-off: those URLs lose ETag/304 caching until responses shrink, but avoid fatal crashes.

Reviewed by Cursor Bugbot for commit 0755dc3. Bugbot is set up for automated code reviews on this repo. Configure here.

…ent OOM

HTTPResultWithETag.serialize() calls JSONObject.toString() which builds the
entire response body into a single contiguous String. For large /offerings
responses (e.g. those containing multiple V2 paywalls), this single allocation
can reach tens of MB and throw OutOfMemoryError on memory-constrained devices
(Java heap limit 128 MB, observed crash at ~37 MB allocation).

Guard storeResult() with a payload-length check against a 4 MB ceiling.
Responses larger than that are served to the caller normally but not written to
SharedPreferences, so the problematic allocation never occurs. The limit is
generous for typical /offerings payloads while well below the crash threshold
seen in production. A WARNING log is emitted when a response is skipped so
operators can detect abnormally large payloads.

Fixes RevenueCat#3628
@tsushanth tsushanth requested a review from a team as a code owner June 26, 2026 02:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OutOfMemoryError in ETagManager.serialize when caching large /offerings response

1 participant