mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 02:32:18 +00:00
Fix session cookie creation race: skip initial token refresh, wrap extension auth hooks (#6563)
Fixes a race causing “No auth header available for session creation” during sign‑in, by skipping the initial token refresh event, and wrapping extension auth hooks with async error handling. Sentry: https://comfy-org.sentry.io/issues/6990347926/?alert_rule_id=1614600&project=4509681221369857 Context - Error surfaced as an unhandled rejection when session creation was triggered without a valid auth header. - Triggers: both onAuthUserResolved and onAuthTokenRefreshed fired during initial login. - Pre‑fix, onIdTokenChanged treated the very first token emission as a “refresh” as well, so two concurrent createSession() calls ran back‑to‑back. - One of those calls could land before a Firebase ID token existed, so getAuthHeader() returned null → createSession threw “No auth header available for session creation”. Exact pre‑fix failure path - src/extensions/core/cloudSessionCookie.ts - onAuthUserResolved → useSessionCookie().createSession() - onAuthTokenRefreshed → useSessionCookie().createSession() - src/stores/firebaseAuthStore.ts - onIdTokenChanged increments tokenRefreshTrigger even for the initial token (treated as a refresh) - getAuthHeader() → getIdToken() may be undefined briefly during initialization - src/platform/auth/session/useSessionCookie.ts - createSession(): calls authStore.getAuthHeader(); if falsy, throws Error('No auth header available for session creation') What this PR changes 1) Skip initial token “refresh” - Track lastTokenUserId and ignore the first onIdTokenChanged for a user; only subsequent token changes count as refresh events. - File: src/stores/firebaseAuthStore.ts 2) Wrap extension auth hooks with async error handling - Use wrapWithErrorHandlingAsync for onAuthUserResolved/onAuthTokenRefreshed/onAuthUserLogout callbacks to avoid unhandled rejections. - File: src/services/extensionService.ts Result - Eliminates the timing window where createSession() runs before getIdToken() returns a token. - Ensures any remaining errors are caught and reported instead of surfacing as unhandled promise rejections. Notes - Lint and typecheck run clean (pnpm lint:fix && pnpm typecheck). ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6563-Fix-session-cookie-creation-race-dedupe-calls-skip-initial-token-refresh-wrap-extensio-2a16d73d365081ef8c22c5ac8cb948aa) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -83,6 +83,7 @@ vi.mock('@/services/dialogService')
|
||||
describe('useFirebaseAuthStore', () => {
|
||||
let store: ReturnType<typeof useFirebaseAuthStore>
|
||||
let authStateCallback: (user: any) => void
|
||||
let idTokenCallback: (user: any) => void
|
||||
|
||||
const mockAuth = {
|
||||
/* mock Auth object */
|
||||
@@ -143,6 +144,55 @@ describe('useFirebaseAuthStore', () => {
|
||||
mockUser.getIdToken.mockResolvedValue('mock-id-token')
|
||||
})
|
||||
|
||||
describe('token refresh events', () => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.doMock('@/platform/distribution/types', () => ({
|
||||
isCloud: true,
|
||||
isDesktop: true
|
||||
}))
|
||||
|
||||
vi.mocked(firebaseAuth.onIdTokenChanged).mockImplementation(
|
||||
(_auth, callback) => {
|
||||
idTokenCallback = callback as (user: any) => void
|
||||
return vi.fn()
|
||||
}
|
||||
)
|
||||
|
||||
vi.mocked(vuefire.useFirebaseAuth).mockReturnValue(mockAuth as any)
|
||||
|
||||
setActivePinia(createPinia())
|
||||
const storeModule = await import('@/stores/firebaseAuthStore')
|
||||
store = storeModule.useFirebaseAuthStore()
|
||||
})
|
||||
|
||||
it("should not increment tokenRefreshTrigger on the user's first ID token event", () => {
|
||||
idTokenCallback?.(mockUser)
|
||||
expect(store.tokenRefreshTrigger).toBe(0)
|
||||
})
|
||||
|
||||
it('should increment tokenRefreshTrigger on subsequent ID token events for the same user', () => {
|
||||
idTokenCallback?.(mockUser)
|
||||
idTokenCallback?.(mockUser)
|
||||
expect(store.tokenRefreshTrigger).toBe(1)
|
||||
})
|
||||
|
||||
it('should not increment when ID token event is for a different user UID', () => {
|
||||
const otherUser = { uid: 'other-user-id' }
|
||||
idTokenCallback?.(mockUser)
|
||||
idTokenCallback?.(otherUser)
|
||||
expect(store.tokenRefreshTrigger).toBe(0)
|
||||
})
|
||||
|
||||
it('should increment after switching to a new UID and receiving a second event for that UID', () => {
|
||||
const otherUser = { uid: 'other-user-id' }
|
||||
idTokenCallback?.(mockUser)
|
||||
idTokenCallback?.(otherUser)
|
||||
idTokenCallback?.(otherUser)
|
||||
expect(store.tokenRefreshTrigger).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
it('should initialize with the current user', () => {
|
||||
expect(store.currentUser).toEqual(mockUser)
|
||||
expect(store.isAuthenticated).toBe(true)
|
||||
|
||||
Reference in New Issue
Block a user