Skip to content

Feat: Cache priority and tests#5

Open
polatolu wants to merge 4 commits intomainfrom
feature/return-cached-flags
Open

Feat: Cache priority and tests#5
polatolu wants to merge 4 commits intomainfrom
feature/return-cached-flags

Conversation

@polatolu
Copy link
Copy Markdown
Collaborator

@polatolu polatolu commented Oct 9, 2025

Description

Fixed the caching mechanism priorities to use cached flags when API calls fails.

Regression Test Recommendations

Type of Change

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ~ ❌ Breaking change (fix or feature that would cause existing functionality to change) ~
  • ~ 🧹 Code refactor ~
  • ~ ✅ Build configuration change ~
  • ~ 📝 Documentation ~
  • ~🗑️ Chore ~

Summary by CodeRabbit

  • New Features

    • Cache-first, identity-aware fallback on API failures with cache-control/TTL-aware validation and safe handling of expired or no-cache responses.
  • Tests

    • Added comprehensive suites covering network/server errors, corrupted cache, TTL/expiry behavior, cache-vs-defaults priority, and identity-specific cache fallbacks.
  • Chores

    • Renamed test target; public API signatures unchanged.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Oct 9, 2025

Walkthrough

Implements cache-first, identity-aware error fallback in Flagsmith: on API fetch failure the client now checks identity-specific cached flags, then general cached flags (with TTL/no-cache validation), then defaultFlags before propagating errors; adds cache helpers, HTTP response date parsing, and comprehensive tests.

Changes

Cohort / File(s) Summary
Core Cache-First Error Handling
FlagsmithClient/Classes/Flagsmith.swift
Adds identity-aware error fallback and cache utilities: handleFlagsErrorForIdentity(...), getCachedFlags(), getCachedFlags(forIdentity:), isCacheValid(cachedResponse:), extractMaxAge(from:), hasNoCacheDirective(in:), HTTP response date parsing helper, and cache decoding/identity helpers.
Package / Test Target Fix
Package.swift
Renames test target from FlagsmitClientTests to FlagsmithClientTests.
Core Error Cache Fallback Tests
FlagsmithClient/Tests/APIErrorCacheFallbackCoreTests.swift
New tests validating core cache-fallback behavior on API failures (cached flags, defaults, or error depending on availability).
Feature-Level Cache Fallback Tests
FlagsmithClient/Tests/APIErrorCacheFallbackFeatureTests.swift
New tests asserting cached feature booleans/values are returned when API fails.
Identity-Level Cache Fallback Tests
FlagsmithClient/Tests/APIErrorCacheFallbackIdentityTests.swift
New tests ensuring identity-specific cached responses are used when identity fetch fails.
Cache Priority Tests
FlagsmithClient/Tests/APIErrorCacheFallbackPriorityTests.swift
New test asserting cache is preferred over defaultFlags when both exist and API fails.
TTL-specific Cache Tests
FlagsmithClient/Tests/APIErrorCacheFallbackTTLTests.swift
New tests validating TTL/expired-cache behavior: expired entries are ignored and defaults used on failure.
Error & Corruption Edge Tests
FlagsmithClient/Tests/APIErrorCacheFallbackErrorTests.swift, FlagsmithClient/Tests/APIErrorCacheFallbackEdgeCaseTests.swift
New suites covering network/server error fallbacks and corrupted cached-response handling (corrupted cache ignored; defaults used).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Flagsmith
    participant Cache
    participant API
    participant Defaults

    Client->>Flagsmith: getFeatureFlags(forIdentity?)
    Flagsmith->>API: fetch (identity-aware)
    API--xFlagsmith: error

    rect rgba(200,230,255,0.35)
        note right of Flagsmith: Cache-first fallback (identity-aware)
        Flagsmith->>Cache: check identity-specific cache
        alt identity cache valid & found
            Cache-->>Flagsmith: identity cached flags
            Flagsmith-->>Client: return identity flags
        else
            Flagsmith->>Cache: check general cache (validate TTL / directives)
            alt general cache valid & found
                Cache-->>Flagsmith: cached flags
                Flagsmith-->>Client: return cached flags
            else
                Flagsmith->>Defaults: check defaultFlags
                alt defaults exist
                    Defaults-->>Flagsmith: default flags
                    Flagsmith-->>Client: return defaults
                else
                    Flagsmith-->>Client: propagate error
                end
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 I hopped through headers, TTL and store,

When endpoints stumble, I checked caches galore.
Identity first, then general stash,
Defaults keep calm when networks crash.
🥕✨ A rabbit’s nibble saved the flag cache.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "Cache priority and tests" is directly related to the main changes in the pull request. The primary implementation change involves modifying the Flagsmith client to use a multi-path cache-then-default fallback approach when API calls fail, which aligns with the concept of "cache priority." Additionally, the title accurately reflects the substantial test additions, with seven new comprehensive test files validating the cache fallback behavior across various scenarios (core, edge cases, errors, features, identity, priority, and TTL). A teammate scanning the repository history would clearly understand that this PR addresses cache prioritization logic and introduces extensive test coverage.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/return-cached-flags

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fbc9a2 and 2d6958b.

📒 Files selected for processing (1)
  • FlagsmithClient/Classes/Flagsmith.swift (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
FlagsmithClient/Classes/Flagsmith.swift (3)
FlagsmithClient/Classes/Internal/APIManager.swift (3)
  • request (131-167)
  • request (174-183)
  • request (191-213)
FlagsmithClient/Classes/Internal/Router.swift (1)
  • request (81-107)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: macos-latest
🔇 Additional comments (5)
FlagsmithClient/Classes/Flagsmith.swift (5)

173-215: LGTM! Well-structured error fallback chain.

The cache-then-default error handling logic is correctly implemented. The identity-specific handler properly tries identity cache → general cache → defaults → error, while the general handler tries cache → defaults → error. This matches the PR objectives and provides robust fallback behavior.


217-268: Cache retrieval methods look solid.

Both cache retrieval methods properly:

  • Use guard statements to avoid force unwraps
  • Log decode failures with descriptive messages
  • Validate cache before attempting decode
  • Return nil gracefully on any failure

The fixes from previous reviews (guard instead of force unwrap at line 245, logging at lines 231 and 261) have been correctly applied.


270-304: Excellent cache validation logic.

The cache validation properly implements the complete decision tree:

  1. Rejects responses with no-cache/no-store directives
  2. Validates max-age from Cache-Control when present
  3. Falls back to configured TTL validation
  4. Treats TTL of 0 as infinite (per line 573 documentation)

The conservative approach of returning false when Date header is missing (line 298) prevents serving potentially stale data. All previous review concerns have been addressed.


306-331: Helper methods correctly handle Cache-Control parsing.

Both helpers are well-implemented:

  • extractMaxAge properly parses the max-age directive and returns nil for invalid values
  • hasNoCacheDirective correctly handles parameterized directives (e.g., no-cache="Set-Cookie") by splitting on both "=" and ";" to extract the base token before comparison

The line 322 logic successfully handles all directive forms, addressing the previous review feedback about parameterized cache-control directives.


336-344: HTTPURLResponse extension correctly configured for HTTP date parsing.

The shared dateFormatter is properly configured:

  • Uses RFC 7231-compliant format for HTTP dates
  • en_US_POSIX locale ensures consistent parsing across device locales
  • GMT timezone matches HTTP date specification
  • Static property provides good performance by reusing a single instance

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gazreese gazreese changed the title Cache priority and tests Feat: Cache priority and tests Oct 9, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 702d5a5 and a06e008.

📒 Files selected for processing (3)
  • FlagsmithClient/Classes/Flagsmith.swift (2 hunks)
  • FlagsmithClient/Tests/APIErrorCacheFallbackTests.swift (1 hunks)
  • Package.swift (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
FlagsmithClient/Classes/Flagsmith.swift (3)
FlagsmithClient/Classes/Internal/APIManager.swift (3)
  • request (131-167)
  • request (174-183)
  • request (191-213)
FlagsmithClient/Classes/Internal/Router.swift (1)
  • request (81-107)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Tests/APIErrorCacheFallbackTests.swift (2)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Classes/Flagsmith.swift (3)
  • getFeatureFlags (126-171)
  • hasFeatureFlag (321-339)
  • getValueForFeature (374-392)
🪛 GitHub Check: swift-lint
FlagsmithClient/Tests/APIErrorCacheFallbackTests.swift

[failure] 12-12:
Type Body Length Violation: Class body should span 350 lines or less excluding comments and whitespace: currently spans 360 lines (type_body_length)


[failure] 346-346:
Force Try Violation: Force tries should be avoided (force_try)


[failure] 59-59:
Force Try Violation: Force tries should be avoided (force_try)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Conventional Commit
  • GitHub Check: macos-latest

Comment on lines +59 to +70
let jsonData = try! JSONEncoder().encode(flags)
let httpResponse = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: [
"Content-Type": "application/json",
"Cache-Control": "max-age=300"
]
)!
return CachedURLResponse(response: httpResponse, data: jsonData)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Replace try! with a throwing helper

The try! on Line 59 will crash the suite on bad fixture data and violates the force_try lint rule. Make this helper throw and propagate the error through the tests (they already declare throws), e.g.:

-    private func createMockCachedResponse(for request: URLRequest, with flags: [Flag]) -> CachedURLResponse {
-        let jsonData = try! JSONEncoder().encode(flags)
+    private func createMockCachedResponse(for request: URLRequest, with flags: [Flag]) throws -> CachedURLResponse {
+        let jsonData = try JSONEncoder().encode(flags)
         let httpResponse = HTTPURLResponse(
             url: request.url!,
             statusCode: 200,
             httpVersion: "HTTP/1.1",
             headerFields: [
                 "Content-Type": "application/json",
                 "Cache-Control": "max-age=300"
             ]
         )!
         return CachedURLResponse(response: httpResponse, data: jsonData)
     }

Then update call sites to try createMockCachedResponse(...). This clears the lint error and keeps failures reported as test failures instead of crashes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let jsonData = try! JSONEncoder().encode(flags)
let httpResponse = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: [
"Content-Type": "application/json",
"Cache-Control": "max-age=300"
]
)!
return CachedURLResponse(response: httpResponse, data: jsonData)
}
private func createMockCachedResponse(for request: URLRequest, with flags: [Flag]) throws -> CachedURLResponse {
let jsonData = try JSONEncoder().encode(flags)
let httpResponse = HTTPURLResponse(
url: request.url!,
statusCode: 200,
httpVersion: "HTTP/1.1",
headerFields: [
"Content-Type": "application/json",
"Cache-Control": "max-age=300"
]
)!
return CachedURLResponse(response: httpResponse, data: jsonData)
}
🧰 Tools
🪛 GitHub Check: swift-lint

[failure] 59-59:
Force Try Violation: Force tries should be avoided (force_try)

🤖 Prompt for AI Agents
In FlagsmithClient/Tests/APIErrorCacheFallbackTests.swift around lines 59 to 70,
the helper currently uses `try! JSONEncoder().encode(flags)` which will crash on
bad fixture data and violates the `force_try` lint rule; change the helper to be
a throwing function (e.g. add `throws` to its signature and use `try` instead of
`try!` when encoding), propagate the thrown error to callers (tests already
declare `throws`), and update all call sites to use `try
createMockCachedResponse(...)` so failures are reported as test failures rather
than runtime crashes.

Comment on lines +346 to +348
let jsonData = try! JSONEncoder().encode(cachedFlags)
let expiredCachedResponse = CachedURLResponse(response: httpResponse, data: jsonData)
testCache.storeCachedResponse(expiredCachedResponse, for: mockRequest)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove the second try!

Line 346 also uses try!, tripping the same lint rule. Because this test already throws, just use try:

-        let jsonData = try! JSONEncoder().encode(cachedFlags)
+        let jsonData = try JSONEncoder().encode(cachedFlags)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let jsonData = try! JSONEncoder().encode(cachedFlags)
let expiredCachedResponse = CachedURLResponse(response: httpResponse, data: jsonData)
testCache.storeCachedResponse(expiredCachedResponse, for: mockRequest)
let jsonData = try JSONEncoder().encode(cachedFlags)
let expiredCachedResponse = CachedURLResponse(response: httpResponse, data: jsonData)
testCache.storeCachedResponse(expiredCachedResponse, for: mockRequest)
🧰 Tools
🪛 GitHub Check: swift-lint

[failure] 346-346:
Force Try Violation: Force tries should be avoided (force_try)

🤖 Prompt for AI Agents
In FlagsmithClient/Tests/APIErrorCacheFallbackTests.swift around lines 346 to
348, the second use of `try!` when encoding `cachedFlags` should be removed;
since this test function is already throwing, replace `try!
JSONEncoder().encode(cachedFlags)` with `try JSONEncoder().encode(cachedFlags)`
so the thrown error propagates instead of force-unwrapping.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a06e008 and 9663216.

📒 Files selected for processing (8)
  • FlagsmithClient/Classes/Flagsmith.swift (2 hunks)
  • FlagsmithClient/Tests/APIErrorCacheFallbackCoreTests.swift (1 hunks)
  • FlagsmithClient/Tests/APIErrorCacheFallbackEdgeCaseTests.swift (1 hunks)
  • FlagsmithClient/Tests/APIErrorCacheFallbackErrorTests.swift (1 hunks)
  • FlagsmithClient/Tests/APIErrorCacheFallbackFeatureTests.swift (1 hunks)
  • FlagsmithClient/Tests/APIErrorCacheFallbackIdentityTests.swift (1 hunks)
  • FlagsmithClient/Tests/APIErrorCacheFallbackPriorityTests.swift (1 hunks)
  • FlagsmithClient/Tests/APIErrorCacheFallbackTTLTests.swift (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
FlagsmithClient/Classes/Flagsmith.swift (3)
FlagsmithClient/Classes/Internal/APIManager.swift (3)
  • request (131-167)
  • request (174-183)
  • request (191-213)
FlagsmithClient/Classes/Internal/Router.swift (1)
  • request (81-107)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Tests/APIErrorCacheFallbackIdentityTests.swift (2)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Classes/Flagsmith.swift (1)
  • getFeatureFlags (126-171)
FlagsmithClient/Tests/APIErrorCacheFallbackCoreTests.swift (2)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Classes/Flagsmith.swift (1)
  • getFeatureFlags (126-171)
FlagsmithClient/Tests/APIErrorCacheFallbackErrorTests.swift (2)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Classes/Flagsmith.swift (1)
  • getFeatureFlags (126-171)
FlagsmithClient/Tests/APIErrorCacheFallbackPriorityTests.swift (2)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Classes/Flagsmith.swift (1)
  • getFeatureFlags (126-171)
FlagsmithClient/Tests/APIErrorCacheFallbackEdgeCaseTests.swift (3)
FlagsmithClient/Tests/APIErrorCacheFallbackCoreTests.swift (2)
  • setUp (15-30)
  • tearDown (32-38)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Classes/Flagsmith.swift (1)
  • getFeatureFlags (126-171)
FlagsmithClient/Tests/APIErrorCacheFallbackFeatureTests.swift (2)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Classes/Flagsmith.swift (2)
  • hasFeatureFlag (338-356)
  • getValueForFeature (391-409)
FlagsmithClient/Tests/APIErrorCacheFallbackTTLTests.swift (2)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
FlagsmithClient/Classes/Flagsmith.swift (1)
  • getFeatureFlags (126-171)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: macos-latest

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
FlagsmithClient/Classes/Flagsmith.swift (1)

292-302: Consider case-insensitive matching for robustness.

The hasPrefix("max-age=") check is case-sensitive. While directive values are typically lowercase, being case-insensitive would be more robust per HTTP specification recommendations.

     private func extractMaxAge(from cacheControl: String) -> TimeInterval? {
         let components = cacheControl.split(separator: ",")
         for component in components {
             let trimmed = component.trimmingCharacters(in: .whitespaces)
-            if trimmed.hasPrefix("max-age=") {
-                let maxAgeString = String(trimmed.dropFirst(8))
+            let lowercased = trimmed.lowercased()
+            if lowercased.hasPrefix("max-age=") {
+                let maxAgeString = String(lowercased.dropFirst(8))
                 return TimeInterval(maxAgeString)
             }
         }
         return nil
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9663216 and 0fbc9a2.

📒 Files selected for processing (1)
  • FlagsmithClient/Classes/Flagsmith.swift (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
FlagsmithClient/Classes/Flagsmith.swift (3)
FlagsmithClient/Classes/Internal/APIManager.swift (3)
  • request (131-167)
  • request (174-183)
  • request (191-213)
FlagsmithClient/Classes/Internal/Router.swift (1)
  • request (81-107)
FlagsmithClient/Classes/Internal/CachedURLResponse.swift (1)
  • response (14-33)
🔇 Additional comments (5)
FlagsmithClient/Classes/Flagsmith.swift (5)

173-190: LGTM! Clean cache-first error fallback.

The priority logic is clear and correct: cached flags take precedence over default flags, and errors are only propagated when both are unavailable.


192-215: LGTM! Identity-aware fallback chain is well-structured.

The priority correctly attempts identity-specific cache first, then general cache, then default flags, before propagating the error.


304-317: LGTM! Parameterized directives are correctly handled.

The implementation correctly extracts the directive name by splitting on = and ;, which handles parameterized forms like no-cache="set-cookie" and no-store;max-age=0.


322-330: LGTM! Proper HTTP date parsing setup.

The static date formatter follows best practices and uses the correct RFC 7231 format for HTTP Date headers.


151-151: LGTM! Correct use of identity-aware error handler.

Switching to handleFlagsErrorForIdentity enables the multi-layer cache fallback for identity-specific requests.

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.

1 participant