6.1 KiB
Mobile Safari Reload Loop - Final Fix
Problem Description
Users on Safari iPhone experienced endless reload loops on:
- Signup page (
/signup) - Email verification page (
/auth/verify) - Password setup page (
/auth/setup-password)
The server logs showed repeated calls to:
/api/recaptcha-config/api/registration-config
Root Causes Identified
1. Incorrect Reactive Reference in Signup Page
Issue: cardClasses was defined as a ref containing a function instead of the function's result:
// WRONG - causes reactivity issues
const cardClasses = ref(() => {
const classes = ['signup-card'];
// ...
return classes.join(' ');
});
Fix: Execute the function immediately and store the result:
// CORRECT
const cardClasses = ref((() => {
const classes = ['signup-card'];
// ...
return classes.join(' ');
})()); // Note the immediate execution with ()
2. Config Cache Not Persisting Across Component Lifecycles
Issue: The global config cache was using module-level variables that could be reset during Vue's reactivity cycles, causing repeated API calls.
Fix: Use window object for true persistence:
// Use window object for true persistence across component lifecycle
function getGlobalCache(): ConfigCache {
if (typeof window === 'undefined') {
return defaultCache;
}
if (!(window as any).__configCache) {
(window as any).__configCache = defaultCache;
}
return (window as any).__configCache;
}
3. Missing Circuit Breaker Protection
Issue: No protection against rapid successive API calls that could trigger reload loops.
Fix: Implemented circuit breaker with threshold protection:
- Max 5 calls in 10-second window
- Automatic blocking when threshold reached
- Fallback to default values when blocked
Complete Solution Implementation
Files Modified
-
pages/signup.vue- Fixed
cardClassesref definition - Ensured static device detection
- Added initialization flag to prevent multiple setups
- Fixed
-
utils/config-cache.ts- Moved cache storage to
windowobject - Added
getGlobalCache()function for persistent storage - Improved circuit breaker implementation
- Added proper logging for debugging
- Moved cache storage to
-
plugins/04.config-cache-init.client.ts(NEW)- Pre-initializes config cache structure
- Sets up global error handlers to catch reload loops
- Prevents multiple initializations
- Adds unhandled rejection handler
How The Fix Works
1. Plugin Initialization (runs first)
04.config-cache-init.client.tsruns before other plugins- Initializes
window.__configCachestructure - Sets up error handlers to catch potential reload loops
- Marks initialization complete with
window.__configCacheInitialized
2. Config Loading (on-demand)
- When pages need config, they call
loadAllConfigs() - Cache is checked first via
getGlobalCache() - If cached, returns immediately (no API call)
- If not cached, makes API call with circuit breaker protection
- Results stored in
window.__configCachefor persistence
3. Circuit Breaker Protection
- Tracks API call history in time windows
- Blocks calls if threshold exceeded (5 calls in 10 seconds)
- Returns fallback values when blocked
- Prevents cascade failures and reload loops
Testing Instructions
Test on Safari iPhone:
- Clear Safari cache and cookies
- Navigate to
/signup- should load without reload loop - Navigate to
/auth/verify?token=test- should show error without loop - Navigate to
/auth/setup-password?email=test@test.com- should load without loop
Monitor Console Logs:
- Look for
[config-cache-init]messages confirming initialization - Check for
[config-cache] Returning cachedmessages on subsequent loads - Watch for
Circuit breaker activatedif threshold reached
Server Logs:
- Should see initial calls to
/api/recaptcha-configand/api/registration-config - Should NOT see repeated calls in quick succession
- Maximum 2-3 calls per page load (initial + retry if needed)
Prevention Measures
1. Static Detection Pattern
All device detection uses static, non-reactive patterns:
const deviceInfo = getStaticDeviceInfo(); // Called once, never reactive
const containerClasses = ref(getDeviceCssClasses('page-name')); // Computed once
2. Configuration Caching
All configuration loading uses cached utility:
const configs = await loadAllConfigs(); // Uses cache automatically
3. Initialization Flags
Prevent multiple initializations:
let initialized = false;
onMounted(() => {
if (initialized) return;
initialized = true;
// ... initialization code
});
Monitoring
Key Metrics to Watch:
- API Call Frequency:
/api/recaptcha-configand/api/registration-configshould be called max once per session - Page Load Time: Should be under 2 seconds on mobile
- Error Rate: No "Maximum call stack" or recursion errors
- User Reports: No complaints about infinite loading
Debug Commands:
// Check cache status in browser console
console.log(window.__configCache);
console.log(window.__configCacheInitialized);
// Force clear cache (for testing)
window.__configCache = null;
window.__configCacheInitialized = false;
Rollback Plan
If issues persist, rollback changes:
- Remove
plugins/04.config-cache-init.client.ts - Revert
utils/config-cache.tsto previous version - Revert
pages/signup.vuechanges
Long-term Improvements
- Server-side caching: Cache config in Redis/memory on server
- SSR config injection: Inject config during SSR to avoid client calls
- PWA service worker: Cache config in service worker
- Config versioning: Add version check to invalidate stale cache
Conclusion
The mobile Safari reload loop has been resolved through:
- Fixing reactive reference bugs
- Implementing proper persistent caching
- Adding circuit breaker protection
- Setting up global error handlers
The solution is backward compatible and doesn't affect desktop users or other browsers. The fix specifically targets the root causes while maintaining the existing functionality.