El bug: la API respondió bien, pero el usuario vio datos ajenos a su contexto
Una caché suele fallar después de cambiar de usuario, tenant, cuenta activa o permiso. La pantalla solicita el mismo endpoint y la caché responde rápido; el problema es que esa respuesta pertenece al contexto anterior.
La causa no es Angular ni HttpClient. Es una clave que no representa el resultado. Si la respuesta depende de contextId, una clave basada solo en la URL afirma erróneamente que ambas solicitudes son equivalentes.
Clave incompleta (GET /api/summary) |
Clave compuesta (summary:{contextId}) |
|
|---|---|---|
| Contexto A |
GET /api/summary → response A |
summary:context-a → response A |
| Contexto B |
GET /api/summary → HIT: reutiliza la respuesta de A — dato obsoleto
|
summary:context-b → response B |
💡 El post original tiene una demo interactiva de esta comparación — pruébala aquí.
La regla es sencilla: la clave debe incluir cada input que pueda cambiar la respuesta. Normalmente incluye ruta, parámetros normalizados, usuario o tenant actual, idioma, permisos relevantes y versión de datos cuando aplique. No agregues valores decorativos: solo dependencias reales del resultado.
Un servicio pequeño con clave compuesta
Este ejemplo mantiene la caché en memoria. El servicio que conoce el cambio de contexto la invalida explícitamente; no deja que una entrada vieja sobreviva por casualidad.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, shareReplay } from 'rxjs';
interface Summary {
total: number;
}
@Injectable({ providedIn: 'root' })
export class SummaryCache {
private contextId = '';
private readonly entries = new Map<string, Observable<Summary>>();
constructor(private readonly http: HttpClient) {}
setContext(contextId: string): void {
if (contextId === this.contextId) return;
this.contextId = contextId;
this.entries.clear();
}
getSummary(filter: string): Observable<Summary> {
const normalizedFilter = filter.trim().toLowerCase();
const key = `summary:${this.contextId}:${normalizedFilter}`;
const cached = this.entries.get(key);
if (cached) return cached;
const request = this.http
.get<Summary>('/api/summary', { params: { filter: normalizedFilter } })
.pipe(shareReplay({ bufferSize: 1, refCount: true }));
this.entries.set(key, request);
return request;
}
}
El detalle importante no es el Map; es la identidad. Dos contextos o filtros distintos producen claves distintas. setContext borra las entradas cuando la fuente de identidad cambia, lo que también evita conservar datos sensibles más tiempo del necesario.
Si el cambio de usuario crea una aplicación completamente nueva, una caché en memoria puede desaparecer sola. No dependas de ese efecto: el problema reaparece con un selector de tenant, una impersonación administrativa, un cambio de permisos o una caché persistente.
Diagnóstico seguro
Empieza sin exponer datos reales.
- Reproduce con dos contextos de prueba y respuestas sintéticas distinguibles, como
response Ayresponse B. - Registra o inspecciona solo la forma de la clave: ruta, filtros normalizados y un identificador sintético de contexto.
- Cambia de contexto sin recargar la aplicación y confirma si la segunda solicitud obtiene un
HITpara una clave que no contiene el contexto. - Revisa todos los lugares que leen y limpian la caché. Corregir solo un componente deja rutas hermanas vulnerables.
Evita registrar cuerpos de respuestas, tokens o identificadores reales. Para este diagnóstico basta demostrar que dos resultados distintos están usando la misma clave.
Pruebas que detectan la regresión
La prueba mínima verifica identidad e invalidación, no la implementación interna de Map. Cada prueba crea su propia caché y cliente HTTP falso, así que puede ejecutarse de forma aislada.
it('does not reuse a summary after the context changes', () => {
const { cache, fakeHttp } = createSummaryCacheWithFakeHttp();
cache.setContext('context-a');
cache.getSummary('open').subscribe();
cache.setContext('context-b');
cache.getSummary('open').subscribe();
expect(fakeHttp.urls).toEqual(['/api/summary', '/api/summary']);
});
it('reuses the same request inside one context', () => {
const { cache, fakeHttp } = createSummaryCacheWithFakeHttp();
cache.setContext('context-a');
cache.getSummary('open').subscribe();
cache.getSummary(' OPEN ').subscribe();
expect(fakeHttp.urls).toHaveLength(1);
});
Añade un caso por cada dimensión que altere la respuesta: tenant, idioma, filtro, rol o versión. Si una dimensión no aparece en la clave, la prueba debe demostrar que no cambia el resultado; de lo contrario, inclúyela.
Errores frecuentes
| Error | Por qué falla | Corrección mínima |
|---|---|---|
| Usar solo la URL | Dos contextos comparten una entrada | Añadir identidad de contexto a la clave |
| Limpiar solo al cerrar sesión | Un cambio de tenant o permisos mantiene datos anteriores | Invalidar en toda transición de contexto |
| Usar objetos sin normalizar como clave | El orden o el espaciado crean entradas duplicadas | Normalizar valores antes de formar la clave |
| Guardar una caché global sin alcance | Una pantalla comparte datos con otra | Mantener la caché cerca de su dominio de datos |
| Confiar solo en TTL | Durante el TTL el dato equivocado sigue siendo posible | Invalidar de forma explícita al cambiar contexto |
Un TTL controla antigüedad; no corrige identidad. La clave evita que un dato incorrecto entre; la invalidación elimina datos que dejaron de ser válidos.
Lista de verificación
- [ ] La clave contiene todos los inputs que cambian el resultado.
- [ ] Los valores se normalizan antes de crear la clave.
- [ ] Cada transición de usuario, tenant, permiso o cuenta invalida el dominio afectado.
- [ ] Las pruebas cubren un
HITdentro del mismo contexto y unMISSentre contextos. - [ ] Los logs de diagnóstico usan valores sintéticos y no incluyen respuestas ni credenciales.
Publicado originalmente en DevEdge Blog.
Top comments (0)