AI helped me write this... so.. it could be completely wrong. But, since I don't know enough about the way stacks work in iOS I'm going to go with it. 😬
Problem
When a server redirects to a URL that's already in the navigation stack with new session-based content (like flash messages or validation errors), Hotwire Native shows the cached snapshot instead of loading the fresh content. This results in users not seeing important feedback messages.
Steps to Reproduce
- User uses a deep link to visit a URL that they don't have access to. i.e. /no-access
- Server redirects back to
/session/new with a flash error message
- Hotwire Native shows the cached
/session/new from the stack without the flash message
- User sees no error feedback and is confused
Current Behavior
When navigating to a URL that exists in the navigation stack, Hotwire Native restores the cached snapshot, which doesn't include session-based changes like:
- Flash messages
- Validation errors
- CSRF token updates
- Any other session-dependent content
Expected Behavior
Hotwire Native should detect when the same URL has different content and load fresh instead of using the snapshot cache.
Proposed Solution
Add server-driven cache control through response headers that Hotwire Native respects:
1. Rails Side - Add header when content differs
# In ApplicationController or Turbo::Redirection
class ApplicationController < ActionController::Base
after_action :set_turbo_cache_headers
private
def set_turbo_cache_headers
response.headers['X-Turbo-Clear-Cache'] = 'true' if flash.any?
end
end
2. Hotwire Native - Respect cache control headers
// In Session.swift
public func visit(_ visitable: Visitable, options: VisitOptions? = nil, reload: Bool = false) {
// Check for cache control headers
let shouldClearCache = options?.response?.responseHeaders?["X-Turbo-Clear-Cache"] != nil
if reload || shouldClearCache {
initialized = false
markSnapshotCacheAsStale()
}
let visit = makeVisit(for: visitable, options: options ?? VisitOptions())
currentVisit?.cancel()
currentVisit = visit
visit.start()
}
// In NavigationHierarchyController.swift
private func visitProposal(from properties: PathProperties, with controller: UIViewController) -> VisitProposal {
let visit = VisitProposal(
path: properties.href,
action: properties.action,
cachePolicy: extractCachePolicy(from: properties)
)
// ... rest of implementation
}
private func extractCachePolicy(from properties: PathProperties) -> CachePolicy {
// Check response headers for cache directives
if properties.responseHeaders?["X-Turbo-Clear-Cache"] != nil {
return .requireFresh
}
return .default
}
Alternative Solutions
Option A: Content-based cache invalidation
Compare content hashes or ETags to determine if cached content is stale.
Option B: Enhanced VisitOptions
Extend VisitOptions with explicit cache control:
public struct VisitOptions {
public enum CachePolicy {
case useSnapshot // Default
case requireFresh // Always bypass cache
case conditional // Check ETag/Last-Modified
}
public var action: Action
public var cachePolicy: CachePolicy = .useSnapshot
}
Option C: Configurable cache behavior
Add configuration to opt into smarter cache handling:
Hotwire.config.snapshotCachePolicy = .smart // or .aggressive, .conservative
Impact
This change would improve user experience for:
- Form submissions with validation errors
- Authentication flows with error messages
- Any redirect-after-post pattern with flash messages
- Session-dependent content updates
Backwards Compatibility
The proposed solution is fully backwards compatible:
- Without the header, behavior remains unchanged
- Apps can opt-in by sending the header
- No breaking changes to existing API
Related Issues
- Similar to Turbo's handling of
data-turbo-cache="false"
- Related to overall snapshot cache management
- Affects both iOS and Android implementations
Questions for Discussion
- Should this be a Turbo Rails concern that automatically adds headers?
- Should we use existing cache headers (
Cache-Control: no-store) instead of custom headers?
- Should this behavior be configurable at the Hotwire Native level?
- How should this interact with the existing
reload parameter?
Example Implementation
I'd be happy to submit a PR with this implementation if the approach is approved. The change would require coordination between:
- hotwire-native-ios
- hotwire-native-android
- turbo-rails (optional, for automatic header injection)
AI helped me write this... so.. it could be completely wrong. But, since I don't know enough about the way stacks work in iOS I'm going to go with it. 😬
Problem
When a server redirects to a URL that's already in the navigation stack with new session-based content (like flash messages or validation errors), Hotwire Native shows the cached snapshot instead of loading the fresh content. This results in users not seeing important feedback messages.
Steps to Reproduce
/session/newwith a flash error message/session/newfrom the stack without the flash messageCurrent Behavior
When navigating to a URL that exists in the navigation stack, Hotwire Native restores the cached snapshot, which doesn't include session-based changes like:
Expected Behavior
Hotwire Native should detect when the same URL has different content and load fresh instead of using the snapshot cache.
Proposed Solution
Add server-driven cache control through response headers that Hotwire Native respects:
1. Rails Side - Add header when content differs
2. Hotwire Native - Respect cache control headers
Alternative Solutions
Option A: Content-based cache invalidation
Compare content hashes or ETags to determine if cached content is stale.
Option B: Enhanced VisitOptions
Extend
VisitOptionswith explicit cache control:Option C: Configurable cache behavior
Add configuration to opt into smarter cache handling:
Impact
This change would improve user experience for:
Backwards Compatibility
The proposed solution is fully backwards compatible:
Related Issues
data-turbo-cache="false"Questions for Discussion
Cache-Control: no-store) instead of custom headers?reloadparameter?Example Implementation
I'd be happy to submit a PR with this implementation if the approach is approved. The change would require coordination between: