From d9fdb01d9b1f80f98809b36652ed55b24230eb82 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sat, 21 Feb 2026 16:30:01 -0800 Subject: [PATCH] fix: handle failed global subgraph blueprint loading gracefully (#9063) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fix "Failed to load subgraph blueprints Error: [ASSERT] Workflow content should be loaded" error occurring on cloud. ## Changes - **What**: `getGlobalSubgraphData` now throws on API failure instead of returning empty string, global blueprint data is validated before loading, and individual global blueprint errors are properly propagated to the toast/console reporting instead of being silently swallowed. ## Review Focus Two root causes were fixed: 1. `getGlobalSubgraphData` returned `""` on failure — this empty string was set as `originalContent`, which is falsy, triggering the assertion in `ComfyWorkflow.load()`. 2. `loadInstalledBlueprints` used an internal `Promise.allSettled` whose results were discarded, so individual global blueprint failures never reached the error reporting in `fetchSubgraphs`. Fixes COM-15199 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9063-fix-handle-failed-global-subgraph-blueprint-loading-gracefully-30e6d73d3650818d9cc8ecf81cd0264e) by [Unito](https://www.unito.io) --- .../management/stores/comfyWorkflow.ts | 9 ++- src/scripts/api.ts | 11 +++- src/stores/subgraphStore.test.ts | 62 +++++++++++++++++++ src/stores/subgraphStore.ts | 19 ++++-- 4 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/platform/workflow/management/stores/comfyWorkflow.ts b/src/platform/workflow/management/stores/comfyWorkflow.ts index ee539edb6..d44dd6c4d 100644 --- a/src/platform/workflow/management/stores/comfyWorkflow.ts +++ b/src/platform/workflow/management/stores/comfyWorkflow.ts @@ -106,8 +106,13 @@ export class ComfyWorkflow extends UserFile { await super.load({ force }) if (!force && this.isLoaded) return this as this & LoadedComfyWorkflow - if (!this.originalContent) { - throw new Error('[ASSERT] Workflow content should be loaded') + if (this.originalContent == null) { + throw new Error( + `[ASSERT] Workflow content should be loaded for '${this.path}'` + ) + } + if (this.originalContent.trim().length === 0) { + throw new Error(`Workflow content is empty for '${this.path}'`) } const initialState = JSON.parse(this.originalContent) diff --git a/src/scripts/api.ts b/src/scripts/api.ts index 557d36b8e..e34695a27 100644 --- a/src/scripts/api.ts +++ b/src/scripts/api.ts @@ -1181,9 +1181,16 @@ export class ComfyApi extends EventTarget { async getGlobalSubgraphData(id: string): Promise { const resp = await api.fetchApi('/global_subgraphs/' + id) - if (resp.status !== 200) return '' + if (resp.status !== 200) { + throw new Error( + `Failed to fetch global subgraph '${id}': ${resp.status} ${resp.statusText}` + ) + } const subgraph: GlobalSubgraphData = await resp.json() - return subgraph?.data ?? '' + if (!subgraph?.data) { + throw new Error(`Global subgraph '${id}' returned empty data`) + } + return subgraph.data as string } async getGlobalSubgraphs(): Promise> { const resp = await api.fetchApi('/global_subgraphs') diff --git a/src/stores/subgraphStore.test.ts b/src/stores/subgraphStore.test.ts index 5cdc1a8fc..f2bef04c6 100644 --- a/src/stores/subgraphStore.test.ts +++ b/src/stores/subgraphStore.test.ts @@ -225,6 +225,68 @@ describe('useSubgraphStore', () => { }) }) + it('should handle global blueprint with empty data gracefully', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + await mockFetch( + {}, + { + broken_blueprint: { + name: 'Broken Blueprint', + info: { node_pack: 'test_pack' }, + data: '' + } + } + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'Failed to load subgraph blueprint', + expect.any(Error) + ) + expect(store.subgraphBlueprints).toHaveLength(0) + consoleSpy.mockRestore() + }) + + it('should handle global blueprint with rejected data promise gracefully', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + await mockFetch( + {}, + { + failing_blueprint: { + name: 'Failing Blueprint', + info: { node_pack: 'test_pack' }, + data: Promise.reject(new Error('Network error')) as unknown as string + } + } + ) + expect(consoleSpy).toHaveBeenCalledWith( + 'Failed to load subgraph blueprint', + expect.any(Error) + ) + expect(store.subgraphBlueprints).toHaveLength(0) + consoleSpy.mockRestore() + }) + + it('should load valid global blueprints even when others fail', async () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + await mockFetch( + {}, + { + broken: { + name: 'Broken', + info: { node_pack: 'test_pack' }, + data: '' + }, + valid: { + name: 'Valid Blueprint', + info: { node_pack: 'test_pack' }, + data: JSON.stringify(mockGraph) + } + } + ) + expect(consoleSpy).toHaveBeenCalled() + expect(store.subgraphBlueprints).toHaveLength(1) + consoleSpy.mockRestore() + }) + describe('search_aliases support', () => { it('should include search_aliases from workflow extra', async () => { const mockGraphWithAliases = { diff --git a/src/stores/subgraphStore.ts b/src/stores/subgraphStore.ts index 752ed957f..78fac3d13 100644 --- a/src/stores/subgraphStore.ts +++ b/src/stores/subgraphStore.ts @@ -198,13 +198,19 @@ export const useSubgraphStore = defineStore('subgraph', () => { } async function loadInstalledBlueprints() { async function loadGlobalBlueprint([k, v]: [string, GlobalSubgraphData]) { + const data = await v.data + if (typeof data !== 'string' || data.trim().length === 0) { + throw new Error( + `Global blueprint '${v.name}' (${k}) returned empty content` + ) + } const path = SubgraphBlueprint.basePath + v.name + '.json' const blueprint = new SubgraphBlueprint({ path, modified: Date.now(), size: -1 }) - blueprint.originalContent = blueprint.content = await v.data + blueprint.originalContent = blueprint.content = data blueprint.filename = v.name useWorkflowStore().attachWorkflow(blueprint) const loaded = await blueprint.load() @@ -238,16 +244,19 @@ export const useSubgraphStore = defineStore('subgraph', () => { return false return true }) - await Promise.allSettled(filteredEntries.map(loadGlobalBlueprint)) + return Promise.allSettled(filteredEntries.map(loadGlobalBlueprint)) } const userSubs = ( await api.listUserDataFullInfo(SubgraphBlueprint.basePath) ).filter((f) => f.path.endsWith('.json')) - const settled = await Promise.allSettled([ - ...userSubs.map(loadBlueprint), - loadInstalledBlueprints() + const [globalResult, ...userResults] = await Promise.allSettled([ + loadInstalledBlueprints(), + ...userSubs.map(loadBlueprint) ]) + const globalResults = + globalResult.status === 'fulfilled' ? globalResult.value : [] + const settled = [...globalResults, ...userResults] const errors = settled.filter((i) => 'reason' in i).map((i) => i.reason) errors.forEach((e) => console.error('Failed to load subgraph blueprint', e))