Examples
Authorization and token refresh
Regular Short-Lived Tokens
In regular business flows, read the current token from the global headers function. The function runs again before every request, so new requests automatically use the latest token after it changes.
TypeScriptconst api = createRequest({ baseURL: '/api/', headers: () => ({ Authorization: `Bearer ${accessToken()}`, }),});
This fits ordinary short-lived tokens, admin-page tokens, server-injected tokens, and similar scenarios.
JWT Access Token + Refresh Token
JWT-based systems usually have two tokens:
- access token: short-lived and used for normal API calls.
- refresh token: longer-lived and used to obtain a new access token.
Recommended flow:
- Always read the current access token from the
headersfunction. - Convert business-failure envelopes into
RequestErrorin a response interceptor. - In an error interceptor, check
event.phase === 'response'anderror.code === 'TOKEN_EXPIRED'. - Use the refresh token to call the refresh endpoint.
- Update the local access token and refresh token.
- Return a replayed
RequestResultfor the original request, indicating that the error has recovered. - Use
meta.retriedto prevent infinite retries.
TypeScriptlet refreshTask: Promise<Tokens> | null = null;const api = createRequest({ baseURL: '/api/', headers: () => ({ Authorization: `Bearer ${accessToken()}`, }), validateStatus: () => true,});api.interceptors.response.use((result) => { const body = result.data as { code?: string; data?: unknown; message?: string; success?: boolean; }; if (body.success !== false) return body.data; throw createRequestError(body.message ?? 'Business Error', { code: body.code, data: body, request: result.request, response: result.response, status: result.response.status, statusText: result.response.statusText, });});api.interceptors.error.use(async (error, event) => { if (!isRequestError(error) || event.phase !== 'response') return; const request = event.request; if (!request) return; if ( error.code === 'TOKEN_EXPIRED' && request.input !== 'auth/refresh' && !(request.meta as { retried?: boolean } | undefined)?.retried ) { refreshTask ??= api .post<Tokens>('auth/refresh', { refreshToken: refreshToken(), }) .finally(() => { refreshTask = null; }); const tokens = await refreshTask; setAccessToken(tokens.token); setRefreshToken(tokens.refreshToken); return api.send(request.input, { ...request.options, meta: { ...(typeof request.meta === 'object' && request.meta ? request.meta : {}), retried: true, }, }); }});
Relationship with Query
Token refresh is a recovery strategy after request failure, so it belongs in the vanilla-request error interceptor. The response interceptor should only convert business-failure responses into RequestError.
Caching, retries, timeouts, aborts, and state display still belong to vanilla-signal-query.
For example, after an access token expires, the error interceptor can refresh the token and replay the original request. query only cares about the final success or failure result.
If the final result still fails, query applies its own retry and timeout policies and asks vanilla-request to try again until the request succeeds or both layers give up.