You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The || guard only covers the empty string (the case tracked in #2110). Any non-empty value the color library can't parse — "nope", "#12", "rgb(" — throws Unable to parse color from string. Confirmed: new Color("nope") throws.
How the bad value gets in
Styling files are a shareable artifact users exchange, so their contents are effectively untrusted input. core/styling/stylingParser.ts validates the enum fields (lineStyle, border/arrow styles) but every color field is a bare z.string().optional():
So a bogus color passes import validation and is persisted to IndexedDB under user-edge-styles.
Why this is severe: it doesn't recover
labelTextColorFor is called from edgeStyleData, which both the canvas (useRenderedEdges in core/StateProvider/renderedEntities.ts) and the schema view (useSchemaGraphEdges in modules/SchemaGraph/useSchemaGraphData.ts) reach during render.
Neither view has a local error boundary. The throw unwinds to the app-level boundary in DefaultLayout.tsx (FallbackComponent={AppErrorPage}), replacing the entire UI with an error page whose only affordance is a reload.
Reload doesn't help. The value lives in IndexedDB, and App.tsx redirects * to /graph-explorer, so the landing route re-throws immediately.
The recovery path is broken too. Settings → Styles renders components/LabelPreview.tsx, which calls labelTextColorFor on the same value — so the page a user would visit to fix or reset the style also throws.
Net effect: the app is unusable until the user manually clears browser storage.
Steps to Reproduce
Import a styling file containing {"edges": {"SomeEdgeType": {"labelColor": "notacolor"}}}.
Have at least one edge of that type on the canvas, or just open the schema view.
Observe the app-level error page. Reload — it comes straight back. Settings → Styles throws as well.
Expected Behavior
An unparseable color is rejected at import with a clear per-field message, and a value already sitting in storage degrades gracefully to the default label color rather than taking down the app.
Fix
Two parts, one at each boundary:
Validate at import. Replace z.string() with a color-validating refinement for every color field in stylingParser.ts, so a bad file becomes an import-time error with a field path — the parser already reports those well. Matches the "prefer Zod at boundaries" convention in AGENTS.md.
Make labelTextColorFor total. Catch the parse failure and fall back to the default label color, so a value already persisted in a user's IndexedDB can't brick the app.
Also worth fixing here
applyColor in packages/graph-explorer/src/core/icons/iconImageUrl.ts concatenates the same unvalidated color into the SVG root's style attribute (color:${color}). Today the output is well-formed and only ever consumed as a data:image/svg+xml image (cytoscape background-image, and an <image href>), a context where nothing in it executes. But an arbitrary string is being spliced into a CSS declaration list, so a value with a ; in it silently produces extra declarations. The comment there notes that #2105 ("tint everything") may inline this SVG as live DOM, at which point that splicing matters much more. Boundary color validation covers this case as well.
Note that root.style.setProperty("color", color) was tried and rejected: CSSOM normalises #FF0000 to rgb(255, 0, 0), changing the emitted data URI and risking silently dropping valid-but-exotic color values.
Notes
Pre-existing on main. The schema-view-style-perf branch relocated this logic; it did not introduce it.
Description
An unparseable color value in an imported styling file takes down the whole app, and it stays down across reloads.
labelTextColorFor(packages/graph-explorer/src/core/StateProvider/graphElementStyleData.ts:64) does:The
||guard only covers the empty string (the case tracked in #2110). Any non-empty value thecolorlibrary can't parse —"nope","#12","rgb("— throwsUnable to parse color from string. Confirmed:new Color("nope")throws.How the bad value gets in
Styling files are a shareable artifact users exchange, so their contents are effectively untrusted input.
core/styling/stylingParser.tsvalidates the enum fields (lineStyle, border/arrow styles) but every color field is a barez.string().optional():labelColor,labelBorderColor,lineColor(~lines 147-160)color,borderColor(~lines 131-140)So a bogus color passes import validation and is persisted to IndexedDB under
user-edge-styles.Why this is severe: it doesn't recover
labelTextColorForis called fromedgeStyleData, which both the canvas (useRenderedEdgesincore/StateProvider/renderedEntities.ts) and the schema view (useSchemaGraphEdgesinmodules/SchemaGraph/useSchemaGraphData.ts) reach during render.DefaultLayout.tsx(FallbackComponent={AppErrorPage}), replacing the entire UI with an error page whose only affordance is a reload.App.tsxredirects*to/graph-explorer, so the landing route re-throws immediately.components/LabelPreview.tsx, which callslabelTextColorForon the same value — so the page a user would visit to fix or reset the style also throws.Net effect: the app is unusable until the user manually clears browser storage.
Steps to Reproduce
{"edges": {"SomeEdgeType": {"labelColor": "notacolor"}}}.Expected Behavior
An unparseable color is rejected at import with a clear per-field message, and a value already sitting in storage degrades gracefully to the default label color rather than taking down the app.
Fix
Two parts, one at each boundary:
z.string()with a color-validating refinement for every color field instylingParser.ts, so a bad file becomes an import-time error with a field path — the parser already reports those well. Matches the "prefer Zod at boundaries" convention inAGENTS.md.labelTextColorFortotal. Catch the parse failure and fall back to the default label color, so a value already persisted in a user's IndexedDB can't brick the app.Also worth fixing here
applyColorinpackages/graph-explorer/src/core/icons/iconImageUrl.tsconcatenates the same unvalidated color into the SVG root'sstyleattribute (color:${color}). Today the output is well-formed and only ever consumed as adata:image/svg+xmlimage (cytoscapebackground-image, and an<image href>), a context where nothing in it executes. But an arbitrary string is being spliced into a CSS declaration list, so a value with a;in it silently produces extra declarations. The comment there notes that #2105 ("tint everything") may inline this SVG as live DOM, at which point that splicing matters much more. Boundary color validation covers this case as well.Note that
root.style.setProperty("color", color)was tried and rejected: CSSOM normalises#FF0000torgb(255, 0, 0), changing the emitted data URI and risking silently dropping valid-but-exotic color values.Notes
Pre-existing on
main. Theschema-view-style-perfbranch relocated this logic; it did not introduce it.Related Issues
stylingParser.ts, but the blank-color case, which fails silently instead of throwingapplyColorstring splicing more consequentialImportant
Internal only — this issue is maintained by the core team and is not accepting external contributions.