fix: proactively refresh Supabase JWT before API calls
Adds token expiry checking and automatic refresh to prevent intermittent 401 errors when the cached session token expires between interactions. - Check token expiry (60s buffer) before each API call - Add 401 interceptor that retries once with refreshed token - Explicitly enable autoRefreshToken in Supabase client Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,9 @@ import { supabase } from '../lib/supabase'
|
||||
|
||||
const API_BASE = import.meta.env['VITE_API_URL'] ?? ''
|
||||
|
||||
// Refresh token if it expires within this many seconds
|
||||
const TOKEN_EXPIRY_BUFFER_SECONDS = 60
|
||||
|
||||
export class ApiError extends Error {
|
||||
status: number
|
||||
|
||||
@@ -12,15 +15,40 @@ export class ApiError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
async function getAuthHeaders(): Promise<Record<string, string>> {
|
||||
function isTokenExpiringSoon(expiresAt: number): boolean {
|
||||
const nowSeconds = Math.floor(Date.now() / 1000)
|
||||
return expiresAt - nowSeconds < TOKEN_EXPIRY_BUFFER_SECONDS
|
||||
}
|
||||
|
||||
async function getValidAccessToken(): Promise<string | null> {
|
||||
const { data } = await supabase.auth.getSession()
|
||||
if (data.session?.access_token) {
|
||||
return { Authorization: `Bearer ${data.session.access_token}` }
|
||||
const session = data.session
|
||||
|
||||
if (!session) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If token is expired or expiring soon, refresh it
|
||||
if (isTokenExpiringSoon(session.expires_at ?? 0)) {
|
||||
const { data: refreshed, error } = await supabase.auth.refreshSession()
|
||||
if (error || !refreshed.session) {
|
||||
return null
|
||||
}
|
||||
return refreshed.session.access_token
|
||||
}
|
||||
|
||||
return session.access_token
|
||||
}
|
||||
|
||||
async function getAuthHeaders(): Promise<Record<string, string>> {
|
||||
const token = await getValidAccessToken()
|
||||
if (token) {
|
||||
return { Authorization: `Bearer ${token}` }
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
async function request<T>(path: string, options?: RequestInit, isRetry = false): Promise<T> {
|
||||
const authHeaders = await getAuthHeaders()
|
||||
const res = await fetch(`${API_BASE}/api/v1${path}`, {
|
||||
...options,
|
||||
@@ -31,6 +59,14 @@ async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
||||
},
|
||||
})
|
||||
|
||||
// On 401, try refreshing the token and retry once
|
||||
if (res.status === 401 && !isRetry) {
|
||||
const { data: refreshed, error } = await supabase.auth.refreshSession()
|
||||
if (!error && refreshed.session) {
|
||||
return request<T>(path, options, true)
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}))
|
||||
throw new ApiError(res.status, body.detail ?? res.statusText)
|
||||
|
||||
@@ -7,10 +7,7 @@ const isLocalDev = supabaseUrl.includes('localhost')
|
||||
// supabase-js hardcodes /auth/v1 as the auth path prefix, but GoTrue
|
||||
// serves at the root when accessed directly (no API gateway).
|
||||
// This custom fetch strips the prefix for local dev.
|
||||
function localGoTrueFetch(
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
): Promise<Response> {
|
||||
function localGoTrueFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
||||
const url = input instanceof Request ? input.url : String(input)
|
||||
const rewritten = url.replace('/auth/v1/', '/')
|
||||
if (input instanceof Request) {
|
||||
@@ -24,6 +21,10 @@ function createSupabaseClient(): SupabaseClient {
|
||||
return createClient('http://localhost:9999', 'stub-key')
|
||||
}
|
||||
return createClient(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
autoRefreshToken: true,
|
||||
persistSession: true,
|
||||
},
|
||||
...(isLocalDev && {
|
||||
global: { fetch: localGoTrueFetch },
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user