mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
Fixes CORS error when service worker follows redirects to GCS by using mode: 'no-cors' to allow cross-origin fetches without CORS headers. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6277-bugfix-add-mode-no-cors-to-fix-CORS-error-when-following-GCS-redirects-2976d73d36508101a4cbd7b59106dfc3) by [Unito](https://www.unito.io)
180 lines
4.9 KiB
JavaScript
180 lines
4.9 KiB
JavaScript
/**
|
|
* @fileoverview Authentication Service Worker
|
|
* Intercepts /api/view requests and adds Firebase authentication headers.
|
|
* Required for browser-native requests (img, video, audio) that cannot send custom headers.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} AuthHeader
|
|
* @property {string} Authorization - Bearer token for authentication
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} CachedAuth
|
|
* @property {AuthHeader|null} header
|
|
* @property {number} expiresAt - Timestamp when cache expires
|
|
*/
|
|
|
|
const CACHE_TTL_MS = 50 * 60 * 1000 // 50 minutes (Firebase tokens expire in 1 hour)
|
|
|
|
/** @type {CachedAuth|null} */
|
|
let authCache = null
|
|
|
|
/** @type {Promise<AuthHeader|null>|null} */
|
|
let authRequestInFlight = null
|
|
|
|
self.addEventListener('message', (event) => {
|
|
if (event.data.type === 'INVALIDATE_AUTH_HEADER') {
|
|
authCache = null
|
|
authRequestInFlight = null
|
|
}
|
|
})
|
|
|
|
self.addEventListener('fetch', (event) => {
|
|
const url = new URL(event.request.url)
|
|
|
|
if (
|
|
!url.pathname.startsWith('/api/view') &&
|
|
!url.pathname.startsWith('/api/viewvideo')
|
|
) {
|
|
return
|
|
}
|
|
|
|
event.respondWith(
|
|
(async () => {
|
|
try {
|
|
const authHeader = await getAuthHeader()
|
|
|
|
if (!authHeader) {
|
|
return fetch(event.request)
|
|
}
|
|
|
|
const headers = new Headers(event.request.headers)
|
|
for (const [key, value] of Object.entries(authHeader)) {
|
|
headers.set(key, value)
|
|
}
|
|
|
|
// Fetch with manual redirect to handle cross-origin redirects (e.g., GCS signed URLs)
|
|
const response = await fetch(
|
|
new Request(event.request.url, {
|
|
method: event.request.method,
|
|
headers: headers,
|
|
credentials: event.request.credentials,
|
|
cache: 'no-store',
|
|
redirect: 'manual',
|
|
referrer: event.request.referrer,
|
|
integrity: event.request.integrity
|
|
})
|
|
)
|
|
|
|
// Handle redirects to external storage (e.g., GCS signed URLs)
|
|
if (response.type === 'opaqueredirect') {
|
|
// Opaqueredirect: redirect occurred but response is opaque (headers not accessible)
|
|
// Re-fetch the original /api/view URL with redirect: 'follow' and mode: 'no-cors'
|
|
// - mode: 'no-cors' allows cross-origin fetches without CORS headers (GCS doesn't have CORS)
|
|
// - Returns opaque response, which works fine for images/videos/audio
|
|
// - Browser will send auth headers to /api/view (same-origin)
|
|
// - Browser will receive 302 redirect to GCS
|
|
// - Browser will follow redirect using GCS signed URL authentication
|
|
return fetch(event.request.url, {
|
|
method: 'GET',
|
|
headers: headers,
|
|
redirect: 'follow',
|
|
mode: 'no-cors'
|
|
})
|
|
}
|
|
|
|
// Non-opaque redirect (status visible) - shouldn't normally happen with redirect: 'manual'
|
|
// but handle as fallback
|
|
if (response.status === 302 || response.status === 301) {
|
|
const location = response.headers.get('location')
|
|
if (location) {
|
|
// Follow redirect manually - do NOT include auth headers for external URLs
|
|
return fetch(location, {
|
|
method: 'GET',
|
|
redirect: 'follow'
|
|
})
|
|
}
|
|
}
|
|
|
|
return response
|
|
} catch (error) {
|
|
console.error('[Auth SW] Request failed:', error)
|
|
return fetch(event.request)
|
|
}
|
|
})()
|
|
)
|
|
})
|
|
|
|
/**
|
|
* Gets auth header from cache or requests from main thread
|
|
* @returns {Promise<AuthHeader|null>}
|
|
*/
|
|
async function getAuthHeader() {
|
|
// Return cached value if valid
|
|
if (authCache && authCache.expiresAt > Date.now()) {
|
|
return authCache.header
|
|
}
|
|
|
|
// Clear expired cache
|
|
if (authCache) {
|
|
authCache = null
|
|
}
|
|
|
|
// Deduplicate concurrent requests
|
|
if (authRequestInFlight) {
|
|
return authRequestInFlight
|
|
}
|
|
|
|
authRequestInFlight = requestAuthHeaderFromMainThread()
|
|
const header = await authRequestInFlight
|
|
authRequestInFlight = null
|
|
|
|
// Cache the result
|
|
if (header) {
|
|
authCache = {
|
|
header,
|
|
expiresAt: Date.now() + CACHE_TTL_MS
|
|
}
|
|
}
|
|
|
|
return header
|
|
}
|
|
|
|
/**
|
|
* Requests auth header from main thread via MessageChannel
|
|
* @returns {Promise<AuthHeader|null>}
|
|
*/
|
|
async function requestAuthHeaderFromMainThread() {
|
|
const clients = await self.clients.matchAll()
|
|
if (clients.length === 0) {
|
|
return null
|
|
}
|
|
|
|
const messageChannel = new MessageChannel()
|
|
|
|
return new Promise((resolve) => {
|
|
let timeoutId
|
|
|
|
messageChannel.port1.onmessage = (event) => {
|
|
clearTimeout(timeoutId)
|
|
resolve(event.data.authHeader)
|
|
}
|
|
|
|
timeoutId = setTimeout(() => {
|
|
console.error(
|
|
'[Auth SW] Timeout waiting for auth header from main thread'
|
|
)
|
|
resolve(null)
|
|
}, 1000)
|
|
|
|
clients[0].postMessage({ type: 'REQUEST_AUTH_HEADER' }, [
|
|
messageChannel.port2
|
|
])
|
|
})
|
|
}
|
|
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(self.clients.claim())
|
|
})
|