Skip to main content

Interceptores HTTP

Tres interceptores de clase (HttpInterceptor) en src/app/core/interceptors/. Cada archivo exporta además un array *_INTERCEPTOR_PROVIDERS con { provide: HTTP_INTERCEPTORS, useClass: ..., multi: true }, que AppModule agrega a providers. No se usa provideHttpClient(withInterceptors(...)): la app importa HttpClientModule.

Resumen y tabla de errores en Auth, interceptores y RBAC; esta página detalla el contrato exacto. Guards relacionados: Guards.

Orden de la cadena​

AppModule registra AuthTokenInterceptor, ErrorInterceptor y DeviceTokenInterceptor, en ese orden. La petición los atraviesa en ese orden; la respuesta (y los errores) vuelve en el inverso.

Consecuencias:

  • ErrorInterceptor ve la petición ya con los headers de AuthTokenInterceptor (por eso puede leer skip) pero antes de los de DeviceTokenInterceptor.
  • Los errores llegan a ErrorInterceptor sin que AuthTokenInterceptor ni DeviceTokenInterceptor los toquen: ninguno de los dos usa catchError.
  • Además de estos tres, LoadingBarHttpClientModule (barra de progreso) registra su propio interceptor al importarse en AppModule.

AuthTokenInterceptor​

Archivo: auth-token.interceptor.ts. providedIn: 'root'. Dependencias: StorageService, Router, IframeDetectionService.

Headers que añade:

HeaderValorCuándo
Timezone-Offsetnew Date().getTimezoneOffset() (minutos; positivo al oeste de UTC)siempre
Timezone-NameIntl.DateTimeFormat().resolvedOptions().timeZone (p. ej. America/Guayaquil)siempre
x-api-key (TOKEN_HEADER_KEY)session.token de USER_SESSIONsolo si hay token y la petición no lleva skip

Lógica:

  1. Si la petición tiene el header skip (valor truthy), clona con solo los headers de zona horaria y continúa. Sin token.
  2. Si no, lee USER_SESSION de secureStorage (cifrado, ver Servicios) y extrae token.
  3. Con token: req.headers.set('x-api-key', token) más zona horaria.
  4. Sin token: si no es iframe, router.navigate(['/auth/login']); en iframe no redirige (se espera el handshake, ver Frame bridge). En ambos casos la petición sigue su curso solo con los headers de zona horaria.

Quién usa skip: AuthService.login y AuthService.frameLogin (new HttpHeaders({ skip: 'skip' }), core/services/api/auth.service.ts).

Gotchas:

  • El header skip no se elimina: viaja al backend junto con la petición de login.
  • Sin token se navega a login pero se envía igual la petición; el backend responderá 401 y ErrorInterceptor aplicará su propio logout/redirección (doble navegación).
  • En el archivo, let authReq = req; y const authReq dentro del if son variables distintas (la interna sombrea a la externa); es inocuo pero confuso.
  • No hay spec para este interceptor.

ErrorInterceptor​

Archivo: http-error.interceptor.ts. @Injectable() sin providedIn (solo existe por el provider de AppModule). Dependencias: ToastrNotificationService, TranslateService, Store, Router, IframeDetectionService, FrameBridgeService.

Envuelve next.handle(req) con catchError. Las ramas se evalúan en este orden (la primera que coincide gana):

#CondiciónEfectoError que ve el llamador
1Petición con skip y status 401nadael HttpErrorResponse original
2Status 401, en iframeFrameBridgeService.emit('CRM_AUTH_EXPIRED') y navigate(['/auth/reconnect'])new Error('') (mensaje vacío)
2Status 401, fuera de iframedispatch(UserActions.userLogout()) y navigate(['/auth/login'])new Error('')
3Status 403toast error con título words.forbiddenActionTitle y mensaje words.forbiddenAction; sin logout ni redirecciónnew Error(<mensaje traducido de words.forbiddenAction>)
4error.error?.data?.code presente (cualquier otro status)sin toastel HttpErrorResponse original
5Cualquier otro (400, 404, 409 sin code, 500, red caída...)toast error con título literal Error y mensaje error.error.message (o words.unknownError)new Error(<mensaje traducido>)

Notas por rama:

  • Rama 1: un login con credenciales erróneas devuelve 401; se deja intacto para que AuthEffects lea el mensaje real. Sin esta rama el usuario vería un mensaje vacío.
  • Rama 2: el Error('') vacío significa que los catchError de los efectos no deben mostrar el mensaje; ya se resolvió con la redirección.
  • Rama 3: se aplica aunque el cuerpo traiga un data.code, porque se evalúa antes que la rama 4. El spec cubre el caso de caché de permisos obsoleta: el guard dejó pasar, pero el backend (autoridad) respondió 403.
  • Rama 4: introducida por sdd/ticket-lock (decisión D5). Ejemplo real: un 409 con data.code = 'TICKET_LOCKED' llega intacto al efecto o componente, que ramifica por código sin ver un toast genérico. También sirve para META_NOT_CONFIGURED en el OAuth de Instagram.
  • Rama 5: getMessageTest acepta un string (lo pasa por translate.instant) o un objeto multilenguaje (elige el idioma de LanguageService.code). Un error de red (status 0) no trae error.error.message, por lo que muestra words.unknownError. El título Error no está traducido.

Specs: http-error.interceptor.spec.ts (403 con toast distinto, sin logout ni navegación, 401 sigue haciendo logout, 500 genérico, y tres casos de paso de errores tipados). No hay spec de las ramas 1 y 2 en iframe.

DeviceTokenInterceptor​

Archivo: device-token.interceptor.ts. providedIn: 'root'. Dependencias: DeviceTokenService, LanguageService.

HeaderValorCondición
X-Device-TokenDeviceTokenService.getOrCreateDeviceToken()solo si la petición no lo trae ya
X-LangLanguageService.code.value (es por defecto, en, ...)solo si la petición no lo trae ya

Detalles:

  • Si ambos headers ya existen no se clona la petición.
  • El idioma sale del store del tenant (selectLanguage), no de navigator.language; el spec lo comprueba (ignores the browser language).
  • DeviceTokenService guarda un UUID v4 en localStorage bajo backoffice_device_token. Si localStorage no está disponible (modo privado) el UUID no persiste. Genera con crypto.randomUUID, con respaldo a getRandomValues y, en último caso, con un valor fallback-<timestamp>-<random>. rotateDeviceToken existe pero no se invoca desde ningún sitio fuera de pruebas.
  • Se aplica también a las peticiones skip (login), por eso el backend recibe el token de dispositivo desde el primer login.

Specs: device-token.interceptor.spec.ts (3 casos: envía X-Lang y X-Device-Token, ignora el idioma del navegador, no pisa un X-Lang ya presente).

Resumen de headers por petición​

HeaderOrigenPetición normalPetición skip
x-api-keyAuthTokensi hay tokenno
Timezone-Offset, Timezone-NameAuthTokensisi
skipllamadornosi (no se elimina)
X-Device-Token, X-LangDeviceTokensi (salvo que ya existan)si