Compare commits

...

8 Commits

Author SHA1 Message Date
bymyself
b8b5455820 test: lock in /assets/ path prefix as part of the contract
Add a hashed CSS asset under a non-/assets/ path to assert the path
prefix is required, not just the extension.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9945#discussion_r3158251424
2026-06-29 20:32:45 -07:00
bymyself
a30138edee test: use realistic dynamic-import message for image false case
Mirror the font case: images arrive in dynamic-import error envelopes,
not CSS-preload ones.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9945#discussion_r3158251423
2026-06-29 20:32:04 -07:00
bymyself
a3e0d376b8 test: use realistic dynamic-import message for font false case
Vite reports non-CSS assets via 'Failed to fetch dynamically imported
module:', not 'Unable to preload CSS for'.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9945#discussion_r3158251419
2026-06-29 20:31:24 -07:00
bymyself
f0e64d3444 refactor: drop redundant fileType check in isStaleChunkError
The HASHED_ASSET_RE already anchors the extension to (js|mjs|css), so
the fileType early-return is fully subsumed by the regex.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9945#discussion_r3158251415
2026-06-29 20:30:40 -07:00
bymyself
a44f60282b fix: match base64 chunk hashes in stale chunk detection
Rollup 4 defaults to base64 hashCharacters, producing URL-safe hashes
with uppercase letters, underscores, and dashes. The lowercase-hex-only
pattern would miss real production chunks and silently suppress the
toast. Broaden the hash class and add a regression test.

Addresses review feedback:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9945#discussion_r3158251412
2026-06-29 20:30:12 -07:00
bymyself
452fe5d781 fix: import useToastStore and useI18n in App.vue
The preload-error toast referenced useToastStore() and t() without
importing them, breaking the typecheck CI gate.
2026-06-29 20:29:28 -07:00
Alexander Brown
c2c0e217c0 Merge branch 'main' into fix/error-toast 2026-04-28 19:17:28 -07:00
bymyself
e02776c793 fix: only show preload error toast for stale chunk errors
Add isStaleChunkError() filter that checks for hashed JS/CSS/MJS assets
under /assets/ before showing the toast. Non-asset URLs (e.g. /api/i18n)
and general network errors no longer trigger the toast. Logging and
Sentry reporting remain unconditional.
2026-03-14 22:56:04 -07:00
3 changed files with 121 additions and 13 deletions

View File

@@ -8,16 +8,19 @@
import { captureException } from '@sentry/vue'
import BlockUI from 'primevue/blockui'
import { computed, onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import config from '@/config'
import { isDesktop } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { electronAPI } from '@/utils/envUtil'
import { parsePreloadError } from '@/utils/preloadErrorUtil'
import { isStaleChunkError, parsePreloadError } from '@/utils/preloadErrorUtil'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
const { t } = useI18n()
const workspaceStore = useWorkspaceStore()
app.extensionManager = useWorkspaceStore()
@@ -92,17 +95,14 @@ onMounted(() => {
}
})
}
// Disabled: Third-party custom node extensions frequently trigger this toast
// (e.g., bare "vue" imports, wrong relative paths to scripts/app.js, missing
// core dependencies). These are plugin bugs, not ComfyUI core failures, but
// the generic error message alarms users and offers no actionable guidance.
// The console.error above still logs the details for developers to debug.
// useToastStore().add({
// severity: 'error',
// summary: t('g.preloadErrorTitle'),
// detail: t('g.preloadError'),
// life: 10000
// })
if (isStaleChunkError(info)) {
useToastStore().add({
severity: 'error',
summary: t('g.preloadErrorTitle'),
detail: t('g.preloadError'),
life: 10000
})
}
})
// Capture resource load failures (CSS, scripts) in non-localhost distributions

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { parsePreloadError } from './preloadErrorUtil'
import { isStaleChunkError, parsePreloadError } from './preloadErrorUtil'
describe('parsePreloadError', () => {
it('parses CSS preload error', () => {
@@ -90,3 +90,96 @@ describe('parsePreloadError', () => {
expect(result.chunkName).toBe('index')
})
})
describe('isStaleChunkError', () => {
it('returns true for hashed JS chunk under /assets/', () => {
const info = parsePreloadError(
new Error(
'Failed to fetch dynamically imported module: /assets/vendor-vue-core-abc123.js'
)
)
expect(isStaleChunkError(info)).toBe(true)
})
it('returns true for hashed CSS chunk under /assets/', () => {
const info = parsePreloadError(
new Error('Unable to preload CSS for /assets/style-9f8e7d.css')
)
expect(isStaleChunkError(info)).toBe(true)
})
it('returns true for hashed mjs chunk under /assets/', () => {
const info = parsePreloadError(
new Error(
'Failed to fetch dynamically imported module: /assets/chunk-abc123.mjs'
)
)
expect(isStaleChunkError(info)).toBe(true)
})
it('returns true for base64 (mixed-case) hashed chunk under /assets/', () => {
const info = parsePreloadError(
new Error(
'Failed to fetch dynamically imported module: /assets/vendor-Bx7_ab2.js'
)
)
expect(isStaleChunkError(info)).toBe(true)
})
it('returns false for non-asset URLs like /api/i18n', () => {
const info = parsePreloadError(
new Error(
'Failed to fetch dynamically imported module: https://cloud.comfy.org/api/i18n'
)
)
expect(isStaleChunkError(info)).toBe(false)
})
it('returns false for unhashed asset files', () => {
const info = parsePreloadError(
new Error('Failed to fetch dynamically imported module: /assets/index.js')
)
expect(isStaleChunkError(info)).toBe(false)
})
it('returns false for hashed assets outside /assets/', () => {
const info = parsePreloadError(
new Error(
'Failed to fetch dynamically imported module: /static/style-abc123.css'
)
)
expect(isStaleChunkError(info)).toBe(false)
})
it('returns false when no URL can be extracted', () => {
const info = parsePreloadError(new Error('Something failed'))
expect(isStaleChunkError(info)).toBe(false)
})
it('returns false for font files', () => {
const info = parsePreloadError(
new Error(
'Failed to fetch dynamically imported module: /assets/inter-abc123.woff2'
)
)
expect(isStaleChunkError(info)).toBe(false)
})
it('returns false for image files', () => {
const info = parsePreloadError(
new Error(
'Failed to fetch dynamically imported module: /assets/logo-abc123.png'
)
)
expect(isStaleChunkError(info)).toBe(false)
})
it('returns true for full URL with hashed asset path', () => {
const info = parsePreloadError(
new Error(
'Failed to fetch dynamically imported module: https://cloud.comfy.org/assets/vendor-three-def456.js'
)
)
expect(isStaleChunkError(info)).toBe(true)
})
})

View File

@@ -64,6 +64,21 @@ function extractChunkName(url: string): string | null {
return withoutHash || null
}
const HASHED_ASSET_RE = /\/assets\/.+-[A-Za-z0-9_-]{6,}\.(js|mjs|css)$/
/**
* Determines if a preload error is a genuine stale chunk error — i.e. a hashed
* JS/CSS asset under /assets/ that 404'd, typically after a new deployment
* changed chunk hashes. Returns false for non-asset URLs (e.g. /api/i18n),
* unknown file types, and errors with no extractable URL.
*/
export function isStaleChunkError(info: PreloadErrorInfo): boolean {
if (!info.url) return false
const pathname = new URL(info.url, 'https://cloud.comfy.org').pathname
return HASHED_ASSET_RE.test(pathname)
}
export function parsePreloadError(error: Error): PreloadErrorInfo {
const message = error.message || String(error)
const url = extractUrl(message)