mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-27 03:19:56 +00:00
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)
216 lines
6.6 KiB
TypeScript
216 lines
6.6 KiB
TypeScript
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
|
import { useErrorHandling } from '@/composables/useErrorHandling'
|
|
import { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'
|
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
|
import { api } from '@/scripts/api'
|
|
import { app } from '@/scripts/app'
|
|
import { useCommandStore } from '@/stores/commandStore'
|
|
import { useExtensionStore } from '@/stores/extensionStore'
|
|
import { KeybindingImpl, useKeybindingStore } from '@/stores/keybindingStore'
|
|
import { useMenuItemStore } from '@/stores/menuItemStore'
|
|
import { useWidgetStore } from '@/stores/widgetStore'
|
|
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
|
|
import type { ComfyExtension } from '@/types/comfy'
|
|
import type { AuthUserInfo } from '@/types/authTypes'
|
|
|
|
export const useExtensionService = () => {
|
|
const extensionStore = useExtensionStore()
|
|
const settingStore = useSettingStore()
|
|
const keybindingStore = useKeybindingStore()
|
|
const {
|
|
wrapWithErrorHandling,
|
|
wrapWithErrorHandlingAsync,
|
|
toastErrorHandler
|
|
} = useErrorHandling()
|
|
|
|
/**
|
|
* Loads all extensions from the API into the window in parallel
|
|
*/
|
|
const loadExtensions = async () => {
|
|
extensionStore.loadDisabledExtensionNames(
|
|
settingStore.get('Comfy.Extension.Disabled')
|
|
)
|
|
|
|
const extensions = await api.getExtensions()
|
|
|
|
// Need to load core extensions first as some custom extensions
|
|
// may depend on them.
|
|
await import('../extensions/core/index')
|
|
extensionStore.captureCoreExtensions()
|
|
await Promise.all(
|
|
extensions
|
|
.filter((extension) => !extension.includes('extensions/core'))
|
|
.map(async (ext) => {
|
|
try {
|
|
await import(/* @vite-ignore */ api.fileURL(ext))
|
|
} catch (error) {
|
|
console.error('Error loading extension', ext, error)
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Register an extension with the app
|
|
* @param extension The extension to register
|
|
*/
|
|
const registerExtension = (extension: ComfyExtension) => {
|
|
extensionStore.registerExtension(extension)
|
|
|
|
const addKeybinding = wrapWithErrorHandling(
|
|
keybindingStore.addDefaultKeybinding
|
|
)
|
|
const addSetting = wrapWithErrorHandling(settingStore.addSetting)
|
|
|
|
extension.keybindings?.forEach((keybinding) => {
|
|
addKeybinding(new KeybindingImpl(keybinding))
|
|
})
|
|
useCommandStore().loadExtensionCommands(extension)
|
|
useMenuItemStore().loadExtensionMenuCommands(extension)
|
|
extension.settings?.forEach(addSetting)
|
|
useBottomPanelStore().registerExtensionBottomPanelTabs(extension)
|
|
if (extension.getCustomWidgets) {
|
|
// TODO(huchenlei): We should deprecate the async return value of
|
|
// getCustomWidgets.
|
|
void (async () => {
|
|
if (extension.getCustomWidgets) {
|
|
const widgets = await extension.getCustomWidgets(app)
|
|
useWidgetStore().registerCustomWidgets(widgets)
|
|
}
|
|
})()
|
|
}
|
|
|
|
if (extension.onAuthUserResolved) {
|
|
const { onUserResolved } = useCurrentUser()
|
|
const handleUserResolved = wrapWithErrorHandlingAsync(
|
|
(user: AuthUserInfo) => extension.onAuthUserResolved?.(user, app),
|
|
(error) => {
|
|
console.error('[Extension Auth Hook Error]', {
|
|
extension: extension.name,
|
|
hook: 'onAuthUserResolved',
|
|
error
|
|
})
|
|
toastErrorHandler(error)
|
|
}
|
|
)
|
|
onUserResolved((user) => {
|
|
void handleUserResolved(user)
|
|
})
|
|
}
|
|
|
|
if (extension.onAuthTokenRefreshed) {
|
|
const { onTokenRefreshed } = useCurrentUser()
|
|
const handleTokenRefreshed = wrapWithErrorHandlingAsync(
|
|
() => extension.onAuthTokenRefreshed?.(),
|
|
(error) => {
|
|
console.error('[Extension Auth Hook Error]', {
|
|
extension: extension.name,
|
|
hook: 'onAuthTokenRefreshed',
|
|
error
|
|
})
|
|
toastErrorHandler(error)
|
|
}
|
|
)
|
|
onTokenRefreshed(() => {
|
|
void handleTokenRefreshed()
|
|
})
|
|
}
|
|
|
|
if (extension.onAuthUserLogout) {
|
|
const { onUserLogout } = useCurrentUser()
|
|
const handleUserLogout = wrapWithErrorHandlingAsync(
|
|
() => extension.onAuthUserLogout?.(),
|
|
(error) => {
|
|
console.error('[Extension Auth Hook Error]', {
|
|
extension: extension.name,
|
|
hook: 'onAuthUserLogout',
|
|
error
|
|
})
|
|
toastErrorHandler(error)
|
|
}
|
|
)
|
|
onUserLogout(() => {
|
|
void handleUserLogout()
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Invoke an extension callback
|
|
* @param {keyof ComfyExtension} method The extension callback to execute
|
|
* @param {any[]} args Any arguments to pass to the callback
|
|
* @returns
|
|
*/
|
|
const invokeExtensions = (method: keyof ComfyExtension, ...args: any[]) => {
|
|
const results: any[] = []
|
|
for (const ext of extensionStore.enabledExtensions) {
|
|
if (method in ext) {
|
|
try {
|
|
results.push(ext[method](...args, app))
|
|
} catch (error) {
|
|
console.error(
|
|
`Error calling extension '${ext.name}' method '${method}'`,
|
|
{ error },
|
|
{ extension: ext },
|
|
{ args }
|
|
)
|
|
}
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
/**
|
|
* Invoke an async extension callback
|
|
* Each callback will be invoked concurrently
|
|
* @param {string} method The extension callback to execute
|
|
* @param {...any} args Any arguments to pass to the callback
|
|
* @returns
|
|
*/
|
|
const invokeExtensionsAsync = async (
|
|
method: keyof ComfyExtension,
|
|
...args: any[]
|
|
) => {
|
|
return await Promise.all(
|
|
extensionStore.enabledExtensions.map(async (ext) => {
|
|
if (method in ext) {
|
|
try {
|
|
// Set current extension name for legacy compatibility tracking
|
|
if (method === 'setup') {
|
|
legacyMenuCompat.setCurrentExtension(ext.name)
|
|
}
|
|
|
|
const result = await ext[method](...args, app)
|
|
|
|
// Clear current extension after setup
|
|
if (method === 'setup') {
|
|
legacyMenuCompat.setCurrentExtension(null)
|
|
}
|
|
|
|
return result
|
|
} catch (error) {
|
|
// Clear current extension on error too
|
|
if (method === 'setup') {
|
|
legacyMenuCompat.setCurrentExtension(null)
|
|
}
|
|
|
|
console.error(
|
|
`Error calling extension '${ext.name}' method '${method}'`,
|
|
{ error },
|
|
{ extension: ext },
|
|
{ args }
|
|
)
|
|
}
|
|
}
|
|
})
|
|
)
|
|
}
|
|
|
|
return {
|
|
loadExtensions,
|
|
registerExtension,
|
|
invokeExtensions,
|
|
invokeExtensionsAsync
|
|
}
|
|
}
|