BugObservability

Silent Keycloak Token Refresh Failure in Axios Request Interceptor Suppresses Browser-Side Diagnostics

The Axios request interceptor in frontend/src/api/axios.js calls keycloak.updateToken(30) inside a try/catch (or a rejected Promise chain) but does not log the caught error, causing failed token refreshes — such as…

Rootlock SRE Engine 5 min read
Diagnostic brief

At a Glance

The Axios request interceptor in frontend/src/api/axios.js calls keycloak.updateToken(30) inside a try/catch (or a rejected Promise chain) but does not log the caught error, causing failed token refreshes — such as those triggered by an expired session — to be completely invisible in the browser console.

Severity Medium
Confidence Medium
Frequency Unknown
Impact See analysis

Summary

The Axios request interceptor in frontend/src/api/axios.js calls keycloak.updateToken(30) inside a try/catch (or a rejected Promise chain) but does not log the caught error, causing failed token refreshes — such as those triggered by an expired session — to be completely invisible in the browser console. The failure surfaces only indirectly as a 401 response logged at the backend by the OIDC provider. This makes session-expiry bugs significantly harder to diagnose and reproduce in both development and production environments.

Root-Cause Analysis

Confirmed evidence:

  • The request interceptor in frontend/src/api/axios.js wraps keycloak.updateToken(30) in error-handling logic.
  • The catch block (or .catch() handler) does not emit any console output.
  • A failed refresh produces a WARN from OidcProvider in the backend log, confirming the 401 reaches the server.
  • No corresponding browser-side log entry exists for the same event.

Confirmed behavior of keycloak.updateToken(minValidity): keycloak.updateToken(30) returns a Promise. When the underlying Keycloak session has expired (e.g., the refresh token's TTL has elapsed or the SSO session was terminated server-side), the adapter rejects the Promise with an error or resolves it with false. Both conditions indicate the token could not be refreshed and the user must re-authenticate.

Reasonable inference: The interceptor catches the rejection but either re-throws a new generic error or resolves with a fallback, discarding the original error object without logging it. This pattern is a common silent-failure anti-pattern in Promise-based auth middleware:

// Likely current shape (illustrative, based on described behavior)
axios.interceptors.request.use(async (config) => {
  try {
    await keycloak.updateToken(30);
  } catch (err) {
    // err is swallowed — no logging, no rethrow of diagnostic information
  }
  config.headers.Authorization = `Bearer ${keycloak.token}`;
  return config;
});

Because the interceptor proceeds to attach whatever token is currently in keycloak.token (which may be expired) after a failed refresh, the request is dispatched with a stale or absent bearer token. The backend's OIDC middleware then rejects it with 401.

Alternative causes (insufficient evidence to confirm):

  • The Keycloak adapter silently swallowing the error itself before the catch block is reached (less likely; the adapter is well-documented to reject on refresh failure).
  • A network error during the /token endpoint call being conflated with a session-expiry error — these should be differentiated in the fix.

Resolution Steps

  1. Add diagnostic logging to the catch block in the Axios request interceptor. At minimum, emit a console.warn so the failure is visible in browser DevTools without being alarmist in production. For richer observability, forward to your existing frontend error-tracking system (e.g., Sentry, Datadog RUM).
  1. Differentiate error types — distinguish between a deliberate session expiry (refresh token expired) and an unexpected network/adapter error, and handle each explicitly.
  1. Ensure the error is re-thrown after logging so that the request is correctly rejected downstream rather than proceeding with a stale token, which would produce a confusing 401 instead of a meaningful auth error to the caller.
  1. Optionally trigger re-authentication when the refresh definitively fails due to session expiry by calling keycloak.login() or redirecting to the logout flow, depending on your UX requirements.
  1. Review whether keycloak.updateToken resolving to false (token still valid, no refresh needed) is handled separately from a rejection (refresh failed). These are distinct outcomes and should not share the same error path.

Configuration Snippets

Apply the following change to frontend/src/api/axios.js:

axios.interceptors.request.use(
  async (config) => {
    try {
      const refreshed = await keycloak.updateToken(30);
      if (refreshed) {
        // Token was successfully refreshed
      }
      // If refreshed === false, the existing token is still valid — no action needed
    } catch (err) {
      // Token refresh failed — session is likely expired or the SSO session was terminated
      console.warn(
        '[axios interceptor] Keycloak token refresh failed. ' +
        'Session may have expired. User may need to re-authenticate.',
        err
      );

      // Optional: report to frontend error tracking
      // errorTracker.captureException(err, { context: 'keycloak.updateToken' });

      // Optional: force re-login when the refresh definitively fails
      // keycloak.login();

      // Reject the pending request so callers receive a meaningful error
      return Promise.reject(err);
    }

    config.headers.Authorization = `Bearer ${keycloak.token}`;
    return config;
  },
  (error) => Promise.reject(error)
);

> Note: The decision to call keycloak.login() automatically vs. allowing the request to fail with a rejected Promise depends on your application's UX contract. Automatically redirecting is appropriate for server-rendered flows; SPAs with fine-grained error boundaries may prefer to surface a "session expired" UI state instead.

Verification

After applying the fix, verify the following:

  1. Simulate a refresh failure by expiring the Keycloak session server-side (log the user out via the Keycloak admin console, or let the SSO session idle timeout expire) while the frontend tab remains open.
  1. Trigger an API call from the frontend (e.g., navigate to a page that fetches data).
  1. Inspect the browser console. You should now see output similar to:
   [axios interceptor] Keycloak token refresh failed. Session may have expired. User may need to re-authenticate. <Error details>
  1. Confirm the backend still returns a 401 (the behavior is unchanged; only observability improves).
  1. Confirm no regression for the normal flow: perform an API call with a valid, non-expired session and verify no warning is emitted and the request succeeds with a 200.

Rollback indicator: If the warning fires unexpectedly during valid sessions, the minValidity threshold of 30 seconds may be too aggressive for your token TTL configuration. Reduce it or confirm the Keycloak realm's access token lifespan is set appropriately.

Prevention

  • Linting rule — no empty catch blocks: Enforce no-empty (ESLint) and consider @typescript-eslint/no-empty-function if TypeScript is used, to catch silent error suppression at CI time.
  {
    "rules": {
      "no-empty": ["error", { "allowEmptyCatch": false }]
    }
  }
  • Frontend error tracking integration: Route all catch blocks in auth middleware through a centralized error reporter (Sentry, Datadog RUM, etc.) so session-expiry events are visible in observability dashboards even when a developer's console is not open.
  • End-to-end test for session expiry: Add a Playwright or Cypress test that invalidates the Keycloak session mid-flight and asserts that the expected warning is logged and the user is shown an appropriate error or redirect.
  • Structured logging helper: Replace ad-hoc console.warn calls in auth code with a scoped logger utility that can be toggled by log level and forwarded to a remote sink, reducing the chance of future contributors removing or omitting log calls.
  • Code review checklist item: Add "auth interceptors must log all caught errors" to your pull request template or review guidelines to prevent regression of this pattern.
Developer FirstBuilt for engineers solving real problems
Evidence DrivenTechnical claims tied to available evidence
Automation ReadyStructured for CLI, APIs, and workflows
Privacy FocusedNo unnecessary data collection in this article UI
STAY AHEAD OF ISSUES

Get new root-cause analyses in your inbox

Engineering-focused updates. No fake subscriber counts. Unsubscribe anytime.