mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-01 03:31:58 +00:00
fix: use authenticated API for remote config polling (#8266)
## Summary
- Fixes remote config polling to use authenticated API
- Consolidates `loadRemoteConfig` into `refreshRemoteConfig` with auth
control
- Adds unit tests for both auth modes
## Problem
The cloud extension's polling interval was using unauthenticated
`fetch`, causing it to receive only default feature flags instead of
user-specific configurations.
**Root cause:**
1. Bootstrap called `loadRemoteConfig()` (raw `fetch`, no auth) -
correct, auth not initialized yet
2. Extension watch called `refreshRemoteConfig()` (`api.fetchApi`, with
auth) - correct
3. Extension interval called `loadRemoteConfig()` (raw `fetch`, no auth)
- **bug**
## Solution
- Consolidate into single `refreshRemoteConfig()` with optional
`useAuth` parameter (defaults to `true`)
- Bootstrap: `refreshRemoteConfig({ useAuth: false })`
- Polling: `refreshRemoteConfig()` (authenticated by default)
## Test Plan
- Unit tests verify both auth modes
- `pnpm typecheck`, `pnpm lint`, `pnpm test:unit` all pass
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8266-fix-use-authenticated-API-for-remote-config-polling-2f16d73d3650817ea7b0e3a7e3ccf12a)
by [Unito](https://www.unito.io)
This commit is contained in:
@@ -2,7 +2,6 @@ import { watchDebounced } from '@vueuse/core'
|
|||||||
|
|
||||||
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
|
||||||
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
|
||||||
import { loadRemoteConfig } from '@/platform/remoteConfig/remoteConfig'
|
|
||||||
import { refreshRemoteConfig } from '@/platform/remoteConfig/refreshRemoteConfig'
|
import { refreshRemoteConfig } from '@/platform/remoteConfig/refreshRemoteConfig'
|
||||||
import { useExtensionService } from '@/services/extensionService'
|
import { useExtensionService } from '@/services/extensionService'
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ useExtensionService().registerExtension({
|
|||||||
{ debounce: 256, immediate: true }
|
{ debounce: 256, immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
// Poll for config updates every 10 minutes
|
// Poll for config updates every 10 minutes (with auth)
|
||||||
setInterval(() => void loadRemoteConfig(), 600_000)
|
setInterval(() => void refreshRemoteConfig(), 600_000)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ import { i18n } from './i18n'
|
|||||||
import { isCloud } from '@/platform/distribution/types'
|
import { isCloud } from '@/platform/distribution/types'
|
||||||
|
|
||||||
if (isCloud) {
|
if (isCloud) {
|
||||||
const { loadRemoteConfig } =
|
const { refreshRemoteConfig } =
|
||||||
await import('@/platform/remoteConfig/remoteConfig')
|
await import('@/platform/remoteConfig/refreshRemoteConfig')
|
||||||
await loadRemoteConfig()
|
await refreshRemoteConfig({ useAuth: false })
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComfyUIPreset = definePreset(Aura, {
|
const ComfyUIPreset = definePreset(Aura, {
|
||||||
|
|||||||
109
src/platform/remoteConfig/refreshRemoteConfig.test.ts
Normal file
109
src/platform/remoteConfig/refreshRemoteConfig.test.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
|
||||||
|
import { refreshRemoteConfig } from './refreshRemoteConfig'
|
||||||
|
import { remoteConfig } from './remoteConfig'
|
||||||
|
|
||||||
|
vi.mock('@/scripts/api', () => ({
|
||||||
|
api: {
|
||||||
|
fetchApi: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
global.fetch = vi.fn()
|
||||||
|
|
||||||
|
describe('refreshRemoteConfig', () => {
|
||||||
|
const mockConfig = { feature1: true, feature2: 'value' }
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
remoteConfig.value = {}
|
||||||
|
window.__CONFIG__ = {}
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with auth (default)', () => {
|
||||||
|
it('uses api.fetchApi when useAuth is true', async () => {
|
||||||
|
vi.mocked(api.fetchApi).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockConfig
|
||||||
|
} as Response)
|
||||||
|
|
||||||
|
await refreshRemoteConfig({ useAuth: true })
|
||||||
|
|
||||||
|
expect(api.fetchApi).toHaveBeenCalledWith('/features', {
|
||||||
|
cache: 'no-store'
|
||||||
|
})
|
||||||
|
expect(global.fetch).not.toHaveBeenCalled()
|
||||||
|
expect(remoteConfig.value).toEqual(mockConfig)
|
||||||
|
expect(window.__CONFIG__).toEqual(mockConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses api.fetchApi by default', async () => {
|
||||||
|
vi.mocked(api.fetchApi).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockConfig
|
||||||
|
} as Response)
|
||||||
|
|
||||||
|
await refreshRemoteConfig()
|
||||||
|
|
||||||
|
expect(api.fetchApi).toHaveBeenCalled()
|
||||||
|
expect(global.fetch).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('without auth', () => {
|
||||||
|
it('uses raw fetch when useAuth is false', async () => {
|
||||||
|
vi.mocked(global.fetch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
json: async () => mockConfig
|
||||||
|
} as Response)
|
||||||
|
|
||||||
|
await refreshRemoteConfig({ useAuth: false })
|
||||||
|
|
||||||
|
expect(global.fetch).toHaveBeenCalledWith('/api/features', {
|
||||||
|
cache: 'no-store'
|
||||||
|
})
|
||||||
|
expect(api.fetchApi).not.toHaveBeenCalled()
|
||||||
|
expect(remoteConfig.value).toEqual(mockConfig)
|
||||||
|
expect(window.__CONFIG__).toEqual(mockConfig)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('error handling', () => {
|
||||||
|
it('clears config on 401 response', async () => {
|
||||||
|
vi.mocked(api.fetchApi).mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 401,
|
||||||
|
statusText: 'Unauthorized'
|
||||||
|
} as Response)
|
||||||
|
|
||||||
|
await refreshRemoteConfig()
|
||||||
|
|
||||||
|
expect(remoteConfig.value).toEqual({})
|
||||||
|
expect(window.__CONFIG__).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears config on 403 response', async () => {
|
||||||
|
vi.mocked(api.fetchApi).mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
status: 403,
|
||||||
|
statusText: 'Forbidden'
|
||||||
|
} as Response)
|
||||||
|
|
||||||
|
await refreshRemoteConfig()
|
||||||
|
|
||||||
|
expect(remoteConfig.value).toEqual({})
|
||||||
|
expect(window.__CONFIG__).toEqual({})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('clears config on fetch error', async () => {
|
||||||
|
vi.mocked(api.fetchApi).mockRejectedValue(new Error('Network error'))
|
||||||
|
|
||||||
|
await refreshRemoteConfig()
|
||||||
|
|
||||||
|
expect(remoteConfig.value).toEqual({})
|
||||||
|
expect(window.__CONFIG__).toEqual({})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -2,9 +2,28 @@ import { api } from '@/scripts/api'
|
|||||||
|
|
||||||
import { remoteConfig } from './remoteConfig'
|
import { remoteConfig } from './remoteConfig'
|
||||||
|
|
||||||
export async function refreshRemoteConfig(): Promise<void> {
|
interface RefreshRemoteConfigOptions {
|
||||||
|
/**
|
||||||
|
* Whether to use authenticated API (default: true).
|
||||||
|
* Set to false during bootstrap before auth is initialized.
|
||||||
|
*/
|
||||||
|
useAuth?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads remote configuration from the backend /features endpoint
|
||||||
|
* and updates the reactive remoteConfig ref
|
||||||
|
*/
|
||||||
|
export async function refreshRemoteConfig(
|
||||||
|
options: RefreshRemoteConfigOptions = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const { useAuth = true } = options
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await api.fetchApi('/features', { cache: 'no-store' })
|
const response = useAuth
|
||||||
|
? await api.fetchApi('/features', { cache: 'no-store' })
|
||||||
|
: await fetch('/api/features', { cache: 'no-store' })
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const config = await response.json()
|
const config = await response.json()
|
||||||
window.__CONFIG__ = config
|
window.__CONFIG__ = config
|
||||||
@@ -19,5 +38,7 @@ export async function refreshRemoteConfig(): Promise<void> {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch remote config:', error)
|
console.error('Failed to fetch remote config:', error)
|
||||||
|
window.__CONFIG__ = {}
|
||||||
|
remoteConfig.value = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
* - Avoiding vendor lock-in for native apps
|
* - Avoiding vendor lock-in for native apps
|
||||||
*
|
*
|
||||||
* This module is tree-shaken in OSS builds.
|
* This module is tree-shaken in OSS builds.
|
||||||
* Used for initial config load in main.ts and polling in the extension.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
@@ -29,26 +28,3 @@ export function configValueOrDefault<K extends keyof RemoteConfig>(
|
|||||||
const configValue = remoteConfig[key]
|
const configValue = remoteConfig[key]
|
||||||
return configValue || defaultValue
|
return configValue || defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads remote configuration from the backend /api/features endpoint
|
|
||||||
* and updates the reactive remoteConfig ref
|
|
||||||
*/
|
|
||||||
export async function loadRemoteConfig(): Promise<void> {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/features', { cache: 'no-store' })
|
|
||||||
if (response.ok) {
|
|
||||||
const config = await response.json()
|
|
||||||
window.__CONFIG__ = config
|
|
||||||
remoteConfig.value = config
|
|
||||||
} else {
|
|
||||||
console.warn('Failed to load remote config:', response.statusText)
|
|
||||||
window.__CONFIG__ = {}
|
|
||||||
remoteConfig.value = {}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch remote config:', error)
|
|
||||||
window.__CONFIG__ = {}
|
|
||||||
remoteConfig.value = {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user