feat: add retry logic for session cookie creation with token refresh

- Add retry mechanism with exponential backoff in useSessionCookie
- Support forceRefresh parameter in getAuthHeader and getIdToken
- First attempt uses cached token, retries force token refresh
- Fixes intermittent 'No auth header available' errors during token refresh

Addresses Sentry issue affecting users with authentication timing issues
This commit is contained in:
Jin Yi
2025-11-02 18:40:20 +09:00
parent 693fbbd3e4
commit 9407a7f1a4
2 changed files with 42 additions and 20 deletions

View File

@@ -10,32 +10,49 @@ export const useSessionCookie = () => {
/**
* Creates or refreshes the session cookie.
* Called after login and on token refresh.
* Implements retry logic with token refresh for handling timing issues.
*/
const createSession = async (): Promise<void> => {
if (!isCloud) return
const authStore = useFirebaseAuthStore()
const authHeader = await authStore.getAuthHeader()
if (!authHeader) {
throw new Error('No auth header available for session creation')
}
// Simple retry with forceRefresh for token timing issues
for (let attempt = 0; attempt < 3; attempt++) {
// First attempt uses cached token, retries force refresh
const authHeader = await authStore.getAuthHeader(attempt > 0)
const response = await fetch(api.apiURL('/auth/session'), {
method: 'POST',
credentials: 'include',
headers: {
...authHeader,
'Content-Type': 'application/json'
if (authHeader) {
// Successfully got auth header, proceed with session creation
const response = await fetch(api.apiURL('/auth/session'), {
method: 'POST',
credentials: 'include',
headers: {
...authHeader,
'Content-Type': 'application/json'
}
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(
`Failed to create session: ${errorData.message || response.statusText}`
)
}
return // Success
}
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(
`Failed to create session: ${errorData.message || response.statusText}`
)
// Exponential backoff before retry (except for last attempt)
if (attempt < 2) {
await new Promise((r) => setTimeout(r, Math.pow(2, attempt) * 500))
}
}
// Failed to get auth header after 3 attempts
throw new Error(
'No auth header available for session creation after retries'
)
}
/**

View File

@@ -106,10 +106,12 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
}
})
const getIdToken = async (): Promise<string | undefined> => {
const getIdToken = async (
forceRefresh = false
): Promise<string | undefined> => {
if (!currentUser.value) return
try {
return await currentUser.value.getIdToken()
return await currentUser.value.getIdToken(forceRefresh)
} catch (error: unknown) {
if (
error instanceof FirebaseError &&
@@ -135,14 +137,17 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => {
* 1. Firebase authentication token (if user is logged in)
* 2. API key (if stored in the browser's credential manager)
*
* @param forceRefresh - If true, forces a refresh of the Firebase token
* @returns {Promise<AuthHeader | null>}
* - A LoggedInAuthHeader with Bearer token if Firebase authenticated
* - An ApiKeyAuthHeader with X-API-KEY if API key exists
* - null if neither authentication method is available
*/
const getAuthHeader = async (): Promise<AuthHeader | null> => {
const getAuthHeader = async (
forceRefresh = false
): Promise<AuthHeader | null> => {
// If available, set header with JWT used to identify the user to Firebase service
const token = await getIdToken()
const token = await getIdToken(forceRefresh)
if (token) {
return {
Authorization: `Bearer ${token}`