> ## Documentation Index
> Fetch the complete documentation index at: https://docs-unity.molca.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Cache HTTP responses with local fallback

> Combine HttpClient with local caching (PlayerPrefs or CacheManager) for offline-capable API calls.

**Applies to:** Molca Core

## Overview

This recipe shows how to make HTTP API calls with local caching for offline fallback. You'll create an `HttpRequestAsset`, fetch data asynchronously, cache responses locally via `PlayerPrefs`, and serve stale cache when the network is unavailable. For a proper cache subsystem with TTL, group tagging, and disk storage, use [CacheManager](/core/cache-manager) instead of raw `PlayerPrefs`.

## Prerequisites

* **SDK modules**: Molca Core installed
* **Unity setup**: RuntimeManager configured in your scene
* **Prior knowledge**: [HttpClient](/core/http-client), [Dependency injection](/core/dependency-injection)
* **Recommended**: Understanding of C# async/await patterns

## Step-by-step

### Step 1: Create HttpRequest ScriptableObject

Create an `HttpRequestAsset` to define your API endpoint configuration. This makes the request reusable and configurable in the Inspector.

```csharp theme={null}
// In the Unity Editor:
// 1. Right-click in Project window → Create → Molca → Networking → HTTP Request
// 2. Name it "FetchUserProfileRequest"
// 3. Configure in Inspector:
//    - URL: "/api/user/profile"
//    - Method: GET
//    - Headers: Add "Accept: application/json"
//    - Timeout: 10 seconds
```

**Why this works**: `HttpRequestAsset` is a ScriptableObject that encapsulates endpoint configuration (URL, method, headers, body templates). This separates configuration from code and makes it easy to modify endpoints without recompiling.

### Step 2: Build the complete caching service

Create a service component that fetches from the API, caches locally, handles staleness, and falls back to cached data on errors.

```csharp theme={null}
using UnityEngine;
using Molca;
using Molca.Networking.Http;
using Molca.Events;
using System;

[Serializable]
public class UserProfile
{
    public string userId;
    public string username;
    public string email;
    public int level;
    public long lastLoginTimestamp;
}

public class UserProfileService : MonoBehaviour
{
    [Header("HTTP Configuration")]
    [SerializeField] private HttpRequestAsset fetchProfileRequest;
    
    [Header("Cache Configuration")]
    [SerializeField] private float cacheValiditySeconds = 300f;
    
    [Header("Events")]
    [Inject] private EventDispatcher _eventDispatcher;
    
    private const string PROFILE_KEY = "UserProfile";
    private const string TIMESTAMP_KEY = "UserProfile_Timestamp";
    
    private UserProfile _currentProfile;

    private async void Start()
    {
        await RuntimeManager.WaitForInitialization();
        
        // Serve cached data immediately, refresh in background
        if (LoadCachedProfile())
        {
            Debug.Log($"Cached profile: {_currentProfile.username}");
            if (!IsCacheValid())
                await FetchAndCache();
        }
        else
        {
            await FetchAndCache();
        }
    }

    private async System.Threading.Tasks.Task FetchAndCache()
    {
        try
        {
            var response = await fetchProfileRequest.SendAsync();
            if (!response.IsSuccess) { FallbackToCache(); return; }
            
            _currentProfile = JsonUtility.FromJson<UserProfile>(response.Text);
            if (_currentProfile == null) { FallbackToCache(); return; }
            
            // Cache with timestamp
            PlayerPrefs.SetString(PROFILE_KEY, response.Text);
            PlayerPrefs.SetString(TIMESTAMP_KEY, 
                DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString());
            PlayerPrefs.Save();
        }
        catch (Exception ex)
        {
            Debug.LogWarning($"Fetch failed: {ex.Message}");
            FallbackToCache();
        }
    }

    private void FallbackToCache()
    {
        if (!LoadCachedProfile())
            Debug.LogError("No cached data available");
    }

    private bool LoadCachedProfile()
    {
        if (!PlayerPrefs.HasKey(PROFILE_KEY)) return false;
        _currentProfile = JsonUtility.FromJson<UserProfile>(
            PlayerPrefs.GetString(PROFILE_KEY));
        return _currentProfile != null;
    }

    private bool IsCacheValid()
    {
        if (!PlayerPrefs.HasKey(TIMESTAMP_KEY)) return false;
        long age = DateTimeOffset.UtcNow.ToUnixTimeSeconds() 
            - long.Parse(PlayerPrefs.GetString(TIMESTAMP_KEY));
        return age < cacheValiditySeconds;
    }

    public void ForceRefresh() => _ = FetchAndCache();
}
```

**Why this works**: The "cache-first" pattern loads locally stored data instantly for UI responsiveness, then refreshes from the network in the background. On fetch failure, stale cache is used as fallback — graceful degradation instead of an error state.

The service class in Step 2 above is the complete implementation. For CacheManager-based caching, see [CacheManager](/core/cache-manager).

## Troubleshooting

* **HTTP request fails with "BaseUrl not set"**: Configure the `HttpModule` in [Global Settings](/setup/global-settings) with your API base URL. Alternatively, set `useFullUrl = true` on the `HttpRequestAsset` and provide the complete URL.
* **JSON parsing fails**: Verify that your C# data structure matches the API response format exactly. Field names must match (case-sensitive). Use `[SerializeField]` for private fields or make fields public. For complex JSON, consider using Newtonsoft.Json instead of `JsonUtility`.
* **Cached data not persisting between sessions**: Ensure you call `PlayerPrefs.Save()` after setting values. On some platforms, `PlayerPrefs` may not persist immediately without explicit save. For more robust persistence, consider using `DataManager` with a custom data provider.
* **Cache never invalidates**: Verify that `cacheValiditySeconds` is set to a reasonable value. Check that the timestamp is being saved correctly. Use `Debug.Log` to print cache age and validity checks.
* **Profile loads but events don't fire**: Ensure `EventDispatcher` is injected successfully. Call `await RuntimeManager.WaitForInitialization()` before dispatching events. Verify that subscribers are registered before the event is dispatched.
* **Multiple simultaneous fetches**: The `_isFetching` flag prevents concurrent requests. If you need to queue requests, implement a request queue or use `DataManager`'s built-in queuing.

## Related

* [HttpClient](/core/http-client) — async HTTP requests and ScriptableObject endpoints
* [DataManager](/core/data-manager) — data providers, caching, and model subscriptions
* [Dependency injection](/core/dependency-injection) — injecting HttpClient and DataManager services
* [EventDispatcher](/core/event-dispatcher) — notifying other systems when data is loaded
* [Recipe: Create and dispatch custom events](/recipes/create-custom-events) — using EventDispatcher for notifications
