Hey folks, I’m wiring up a frontend fetch wrapper that does retries + a tiny in-memory cache, and I’m trying to keep the security boundaries sane (no cross-user data, no stale auth). The failure mode I’m scared of is caching a 401/403 or caching a response that was fetched with one token and later served after the user switches accounts.
const cache = new Map();
export async function apiFetch(url, { token, retry = 2 } = {}) {
const key = url; // probably too naive?
if (cache.has(key)) return cache.get(key);
let lastErr;
for (let i = 0; i <= retry; i++) {
try {
const res = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : {}
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
cache.set(key, data);
return data;
} catch (e) {
lastErr = e;
}
}
throw lastErr;
}
What’s a practical way to key/partition and invalidate this so retries and caching don’t accidentally cross auth boundaries or cache “bad” states?
Keying on url alone is basically “community fridge” energy — somebody’s gonna grab the wrong leftovers.
I’d keep it super strict: only cache successful GET/HEAD, and make the key include an “auth epoch” (a counter you bump on login/logout/token refresh/account switch) plus method + url (and yeah, body hash if you ever go beyond GET). Don’t cache 401/403 at all, and if you cache the in-flight promise to dedupe retries, make sure you delete it on rejection so you don’t pin a failure and keep serving sadness.
key by auth scope plus request details, not by the raw token. the token is a secret, not a cache key, and /me absolutely should not share a bucket with /projects?team=foo.
i’d keep it simple: partition by something stable from your auth state, like userId, tenantId, or sessionId, then key by method + full URL + any request bits that change the response. clear that partition on logout or account switch. and don’t cache 401/403 at all — those should just fail and get retried or handled by the auth flow.
the bit people miss is in-flight requests. if you cache only the final json, two calls can race and you end up serving the wrong thing after a switch. caching the promise for the short retry window is fine, but delete it on rejection and keep the ttl short for successful reads.
if you don’t have a stable auth scope, that’s the real problem. don’t try to be clever with the bearer token.