Skip to content

Invalid color in an imported styling file permanently breaks the graph and schema views #2114

Description

@kmcginnes

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:

new Color(labelColor || appDefaultEdgeStyle.labelColor);

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():

  • edge entry — labelColor, labelBorderColor, lineColor (~lines 147-160)
  • vertex entry — 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

  1. 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.
  2. 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.
  3. Reload doesn't help. The value lives in IndexedDB, and App.tsx redirects * to /graph-explorer, so the landing route re-throws immediately.
  4. 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

  1. Import a styling file containing {"edges": {"SomeEdgeType": {"labelColor": "notacolor"}}}.
  2. Have at least one edge of that type on the canvas, or just open the schema view.
  3. 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:

  1. 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.
  2. 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.

Related Issues

Important

Internal only — this issue is maintained by the core team and is not accepting external contributions.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    customizationCustomization options for rendering graph data in non-default waysinternalSignals that the team will work on this issue internally.reliabilityIssues relating to improvements in reliability

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions