@sinequa/atomic

Handling a 401

Recovering an expired session with one hook, the requests that must never use it, and the deadlock the naive version produces.

A session that expires while the user was idle surfaces as a 401 on their next action. Without a hook that 401 simply reaches the caller — the historical behaviour, and a perfectly valid choice. With one, the request is repaired and replayed, and the user notices nothing.

This replaces the HTTP error interceptor that framework integrations used to implement.

the hook
client.configure({
  auth: {
    ...client.config.auth,
    recoverFromUnauthorized: async ({ endpoint, error }) => {
      // Return true once the session is usable again.
      return client.auth.hasSession();
    },
  },
});

What the library does with it

yes no yes no no yes true false or throws a request answers 401 noRecoveryon this request? throw — this 401 is an answer,not a session loss deliberatelysigned out? throw — do not reopena session the user closed a recovery hook? throw UnauthorizedError invoke it — once per client,shared by concurrent 401s replay the request ONCE,with the refreshed CSRF token throw the original error

Four guarantees worth relying on:

  • The request is replayed exactly once. An endpoint that keeps failing cannot loop.
  • Concurrent 401s share one recovery. Three requests failing together produce one hook call and three requests waiting on the same verdict.
  • A hook that throws counts as false. The original 401 is never masked by a failure inside your hook.
  • Without a hook, nothing changes. One round trip, the error surfaces, exactly as before.

A hook that works

Repair the session, and replay
src/session-recovery.ts
import { createAtomicClient } from "@sinequa/atomic";

export const client = createAtomicClient({
  app: "<your-app>",
  backendUrl: "https://<your-sinequa-server>",
});

client.configure({
  auth: {
    // Spread: configure() replaces a nested object wholesale.
    ...client.config.auth,

    recoverFromUnauthorized: async ({ endpoint }) => {
      console.debug("recovering the session for", endpoint);

      // hasSession() never redirects, and its own requests carry noRecovery — so
      // this cannot re-enter the recovery it was called from.
      if (await client.auth.hasSession()) return true;

      // No session to recover. For an ambient or provider deployment, login() can
      // re-establish one; in credentials mode it returns false and the form takes over.
      return client.auth.login().catch(() => false);
    },
  },
});

await client.initializeConfig();
await client.auth.login();

The deadlock, and why noRecovery exists

Wire the hook to your own sign-in and the naive version hangs at start-up. Here is the cycle:

sign-in in flight it issues its own requestsCSRF, probe, webtoken, logout one answers 401 the recovery hook runs it calls the sign-in —already in flight it returns that pending promise which cannot settle untilthe request completes

Nothing rejects. The caller waits forever — in an Angular application, the initializer never settles and the page stays blank with an empty console.

The library breaks the cycle by marking its own sign-in requests noRecovery. Their 401 is the answer — no session, credentials refused, no auto-authentication — never a session to repair:

RequestWhy it is exempt
security.webtokenA wrong password must surface as an error, not trigger a repair
challenge?action=getCsrfTokenIts 401 means "no session", which is what was asked
The auto-authentication probeIts 401 is the negative result being tested for
challenge?action=deleteWebTokenCookieSigning out
Audit.notifyFire-and-forget telemetry; its 401 was never a session

Set it on your own requests too, on anything that participates in establishing a session — in particular a session probe called from inside your own hook:

a probe that cannot re-enter
// fetchPrincipal accepts request options, so a probe can opt out explicitly.
const principal = await client.api.principal.get({ noRecovery: true });

noRecovery is available on every HTTP helper. See The HTTP layer.

Pitfalls

What's next

On this page