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.
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
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 original401is 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
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();import { InjectionToken, inject, provideAppInitializer } from "@angular/core";
import { type AtomicClient, createAtomicClient } from "@sinequa/atomic";
export const SINEQUA = new InjectionToken<AtomicClient>("Sinequa client", {
factory: () =>
createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
}),
});
export function provideSinequa() {
return provideAppInitializer(async () => {
const client = inject(SINEQUA);
client.configure({
auth: {
// Spread: configure() replaces a nested object wholesale.
...client.config.auth,
recoverFromUnauthorized: async () => {
// Never redirects, and its own requests are exempt from recovery — so it
// cannot re-enter the hook it was called from.
if (await client.auth.hasSession()) return true;
return client.auth.login().catch(() => false);
},
},
});
await client.initializeConfig();
await client.auth.login();
});
}import { createAtomicClient } from "@sinequa/atomic";
export const sinequa = createAtomicClient({
app: "<your-app>",
backendUrl: "https://<your-sinequa-server>",
});
sinequa.configure({
auth: {
// Spread: configure() replaces a nested object wholesale.
...sinequa.config.auth,
recoverFromUnauthorized: async () => {
// Never redirects, and its own requests are exempt from recovery — so it
// cannot re-enter the hook it was called from.
if (await sinequa.auth.hasSession()) return true;
return sinequa.auth.login().catch(() => false);
},
},
});
export const ready = (async () => {
await sinequa.initializeConfig();
await sinequa.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:
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:
| Request | Why it is exempt |
|---|---|
security.webtoken | A wrong password must surface as an error, not trigger a repair |
challenge?action=getCsrfToken | Its 401 means "no session", which is what was asked |
| The auto-authentication probe | Its 401 is the negative result being tested for |
challenge?action=deleteWebTokenCookie | Signing out |
Audit.notify | Fire-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:
// 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.