[backport cloud/1.49] fix: tolerate a missing /api/i18n endpoint during bootstrap - #15176
Conversation
## ELI-5
Every time the app starts it asks the server "do any custom nodes ship
translations?". Some backends deliberately don't have that endpoint at
all, so the browser gets a 404 back — and because nobody was catching
it, that 404 turned into a big red unhandled error in the console (and
in error tracking) even though nothing was actually broken. Now we treat
"endpoint isn't there" as "there are no custom-node translations", and
carry on. Real failures still get reported.
## Summary
`bootstrapStore` called `api.getCustomNodesI18n()` unconditionally with
no rejection handling. On a backend that does not implement `GET
/api/i18n` (Cloud vendors no custom-node locale files, so the endpoint
is intentionally absent), the request 404s and the resulting
`AxiosError` reaches `useAsyncState`'s default `onError`, which is
`globalThis.reportError` — i.e. it surfaces as an *unhandled* error to
`window.onerror` and to RUM. In production this fires in ~1,300 distinct
sessions/week (2,406 events over a rolling 7 days), mostly on the login
view before the user has even signed in.
## Changes
- **What**: `bootstrapStore` now routes the fetch through a small
`fetchCustomNodesI18n()` helper that swallows *only* an Axios 404 and
returns `undefined`; everything else is rethrown unchanged and still
lands in `i18nError`. When the endpoint is absent,
`mergeCustomNodesI18n` is skipped and bootstrap proceeds.
- Tests: `src/stores/bootstrapStore.test.ts` gains a 404 case (asserts
`isI18nReady` flips true, `i18nError` stays undefined, merge is skipped)
and a 500 case (asserts the error is still surfaced). Confirmed the 404
test fails against the unpatched store and passes with the fix; the 500
test passes either way, which is the point — it is the guard against
blanket-swallowing.
## Review Focus
- **Skip vs. empty map.** The 404 path skips `mergeCustomNodesI18n`
rather than calling it with `{}`. That is deliberate:
`mergeCustomNodesI18n` clears `customNodesI18nData` before assigning, so
passing an empty map would *wipe* any previously merged custom-node
translations on a re-run rather than being a no-op.
- **Where the tolerance lives.** I put it in the store (the consumer
that owns "translations are optional") rather than in
`ComfyApi.getCustomNodesI18n`, so the transport method stays faithful to
the response and any future caller can still distinguish a 404.
`getCustomNodesI18n` currently has exactly one call site.
- **Tradeoff worth naming.** On a backend that *does* implement the
endpoint but 404s for some unrelated reason (a misconfigured proxy,
say), custom-node translations now fail silently instead of loudly. The
previous behaviour also failed to load them — it just additionally
reported an error — so nothing regresses functionally, but
diagnosability on that path is slightly reduced. Adding a warning log
was considered and rejected: on Cloud it would fire on every session for
a condition that is by design.
- Non-404 failures (5xx, network errors — `error.response` is undefined
for the latter, so it rethrows) are unchanged and still reported.
## Verification
`vitest run src/stores/bootstrapStore.test.ts src/i18n.test.ts` (23
passed), `eslint` and `oxfmt` clean on both changed files. Full-repo
`vue-tsc` was run but is not a usable signal in this worktree — it
borrows the parent clone's `node_modules` via symlink, which breaks SFC
type-reference resolution and produces ~1,400 pre-existing `.vue`
diagnostics unrelated to this change. No diagnostics were reported for
any file under `src/stores/`, including both changed files. Relying on
CI for the whole-repo typecheck gate.
## Provenance
This came out of a goal run against the Cloud frontend
error-free-sessions dashboard ("State of Dog", Datadog RUM, prod),
measured over a rolling 7-day window: error-free sessions were sitting
at 17–22% daily, with 8,662 of 10,790 sessions containing at least one
error. The target is ≥90% daily, and this 404 was the single
highest-session-count contributor. The endpoint's absence was confirmed
server-side (it is tagged local-only and has no handler) before writing
the fix, so this is deliberately a frontend-side tolerance rather than a
request to implement the endpoint.
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1792 passed, 0 failed · 1 flaky📊 Browser Reports
📦 Bundle Size
⚡ Performance Report
Absolute values
Raw data{
"timestamp": "2026-08-13T03:27:56.656Z",
"gitSha": "b75e3e4770ea7fb9d1e285e35c47dd0c83bedabe",
"branch": "backport-14501-to-cloud-1.49",
"measurements": [
{
"name": "canvas-idle",
"durationMs": 2015.1349999999866,
"styleRecalcs": 9,
"styleRecalcDurationMs": 9.921000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 600.2579999999999,
"heapDeltaBytes": 5129592,
"heapUsedBytes": 73816964,
"domNodes": 18,
"jsHeapTotalBytes": 20299776,
"scriptDurationMs": 27.114,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-idle",
"durationMs": 2035.1480000000493,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.783999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 516.0859999999999,
"heapDeltaBytes": 5427832,
"heapUsedBytes": 74157824,
"domNodes": 16,
"jsHeapTotalBytes": 19775488,
"scriptDurationMs": 22.044999999999998,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1891.8260000000373,
"styleRecalcs": 74,
"styleRecalcDurationMs": 40.998999999999995,
"layouts": 12,
"layoutDurationMs": 3.8699999999999997,
"taskDurationMs": 1010.6120000000001,
"heapDeltaBytes": -17846148,
"heapUsedBytes": 50630712,
"domNodes": -281,
"jsHeapTotalBytes": 19902464,
"scriptDurationMs": 138.74499999999998,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-mouse-sweep",
"durationMs": 1839.8799999999937,
"styleRecalcs": 74,
"styleRecalcDurationMs": 40.321999999999996,
"layouts": 12,
"layoutDurationMs": 3.354,
"taskDurationMs": 893.7310000000001,
"heapDeltaBytes": -227112,
"heapUsedBytes": 68266352,
"domNodes": 58,
"jsHeapTotalBytes": 20561920,
"scriptDurationMs": 129.39999999999998,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1755.6349999999838,
"styleRecalcs": 32,
"styleRecalcDurationMs": 17.971,
"layouts": 6,
"layoutDurationMs": 0.6739999999999999,
"taskDurationMs": 487.93399999999997,
"heapDeltaBytes": 8576980,
"heapUsedBytes": 77200168,
"domNodes": 77,
"jsHeapTotalBytes": 19251200,
"scriptDurationMs": 31.936,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "canvas-zoom-sweep",
"durationMs": 1752.791000000002,
"styleRecalcs": 31,
"styleRecalcDurationMs": 16.802999999999997,
"layouts": 6,
"layoutDurationMs": 0.5369999999999999,
"taskDurationMs": 400.45799999999997,
"heapDeltaBytes": 7967960,
"heapUsedBytes": 76547100,
"domNodes": 76,
"jsHeapTotalBytes": 20037632,
"scriptDurationMs": 20.470000000000002,
"eventListeners": 19,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 657.9540000000179,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.309999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 435.21900000000005,
"heapDeltaBytes": -12327944,
"heapUsedBytes": 56163636,
"domNodes": 18,
"jsHeapTotalBytes": 21086208,
"scriptDurationMs": 65.884,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "dom-widget-clipping",
"durationMs": 655.2629999999908,
"styleRecalcs": 11,
"styleRecalcDurationMs": 8.799999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 444.278,
"heapDeltaBytes": -10992996,
"heapUsedBytes": 57592548,
"domNodes": 18,
"jsHeapTotalBytes": 20037632,
"scriptDurationMs": 71.398,
"eventListeners": 2,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.670000000000012,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "large-graph-idle",
"durationMs": 2061.800000000005,
"styleRecalcs": 9,
"styleRecalcDurationMs": 11.088000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 805.8140000000001,
"heapDeltaBytes": 8737656,
"heapUsedBytes": 68610384,
"domNodes": -281,
"jsHeapTotalBytes": 4775936,
"scriptDurationMs": 128.02200000000002,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-idle",
"durationMs": 2025.8549999999786,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.213000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 697.777,
"heapDeltaBytes": 7849644,
"heapUsedBytes": 67785832,
"domNodes": -281,
"jsHeapTotalBytes": 5562368,
"scriptDurationMs": 110.598,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2474.9199999999973,
"styleRecalcs": 67,
"styleRecalcDurationMs": 13.732000000000001,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1496.3509999999999,
"heapDeltaBytes": 8167788,
"heapUsedBytes": 69298616,
"domNodes": -288,
"jsHeapTotalBytes": 5263360,
"scriptDurationMs": 502.687,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-pan",
"durationMs": 2288.663999999926,
"styleRecalcs": 68,
"styleRecalcDurationMs": 13.303000000000003,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 1355.407,
"heapDeltaBytes": -5333792,
"heapUsedBytes": 55472448,
"domNodes": -284,
"jsHeapTotalBytes": 5562368,
"scriptDurationMs": 456.925,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "large-graph-zoom",
"durationMs": 3579.4970000000035,
"styleRecalcs": 63,
"styleRecalcDurationMs": 14.882,
"layouts": 60,
"layoutDurationMs": 7.762999999999999,
"taskDurationMs": 1773.815,
"heapDeltaBytes": -8306564,
"heapUsedBytes": 54164532,
"domNodes": -290,
"jsHeapTotalBytes": 7135232,
"scriptDurationMs": 629.124,
"eventListeners": -151,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "large-graph-zoom",
"durationMs": 3414.3130000001065,
"styleRecalcs": 64,
"styleRecalcDurationMs": 14.840000000000002,
"layouts": 60,
"layoutDurationMs": 7.633,
"taskDurationMs": 1667.9969999999998,
"heapDeltaBytes": 11341496,
"heapUsedBytes": 73486736,
"domNodes": -288,
"jsHeapTotalBytes": 6610944,
"scriptDurationMs": 583.5169999999999,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "minimap-idle",
"durationMs": 2059.8439999999982,
"styleRecalcs": 8,
"styleRecalcDurationMs": 7.123999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 867.953,
"heapDeltaBytes": 5416176,
"heapUsedBytes": 67365228,
"domNodes": -283,
"jsHeapTotalBytes": 4251648,
"scriptDurationMs": 133.655,
"eventListeners": -179,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "minimap-idle",
"durationMs": 2033.6340000000064,
"styleRecalcs": 9,
"styleRecalcDurationMs": 7.698,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 663.7839999999999,
"heapDeltaBytes": 8345508,
"heapUsedBytes": 70063900,
"domNodes": -284,
"jsHeapTotalBytes": 5300224,
"scriptDurationMs": 102.62599999999999,
"eventListeners": -149,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 676.288999999997,
"styleRecalcs": 45,
"styleRecalcDurationMs": 10.605999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 443.89,
"heapDeltaBytes": -11376028,
"heapUsedBytes": 57411884,
"domNodes": 16,
"jsHeapTotalBytes": 21086208,
"scriptDurationMs": 128.77700000000002,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-dom-widget-clipping",
"durationMs": 672.4540000000161,
"styleRecalcs": 46,
"styleRecalcDurationMs": 12.113999999999999,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 445.41499999999996,
"heapDeltaBytes": -11404964,
"heapUsedBytes": 57434156,
"domNodes": 18,
"jsHeapTotalBytes": 21348352,
"scriptDurationMs": 130.502,
"eventListeners": 8,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "subgraph-idle",
"durationMs": 2021.6230000000337,
"styleRecalcs": 9,
"styleRecalcDurationMs": 8.67,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 577.701,
"heapDeltaBytes": -21885772,
"heapUsedBytes": 46712800,
"domNodes": -280,
"jsHeapTotalBytes": 19640320,
"scriptDurationMs": 19.364,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.699999999999818
},
{
"name": "subgraph-idle",
"durationMs": 2040.687000000048,
"styleRecalcs": 8,
"styleRecalcDurationMs": 9.287,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 496.5539999999999,
"heapDeltaBytes": 5143052,
"heapUsedBytes": 73907000,
"domNodes": 16,
"jsHeapTotalBytes": 20561920,
"scriptDurationMs": 15.064,
"eventListeners": 6,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1739.3349999999828,
"styleRecalcs": 76,
"styleRecalcDurationMs": 42.004,
"layouts": 16,
"layoutDurationMs": 4.913,
"taskDurationMs": 861.344,
"heapDeltaBytes": -4042892,
"heapUsedBytes": 64755548,
"domNodes": 63,
"jsHeapTotalBytes": 20299776,
"scriptDurationMs": 105.378,
"eventListeners": 4,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-mouse-sweep",
"durationMs": 1736.6040000000567,
"styleRecalcs": 78,
"styleRecalcDurationMs": 36.62,
"layouts": 16,
"layoutDurationMs": 4.446,
"taskDurationMs": 808.676,
"heapDeltaBytes": -22241816,
"heapUsedBytes": 46533180,
"domNodes": 6,
"jsHeapTotalBytes": 19902464,
"scriptDurationMs": 88.544,
"eventListeners": -153,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333335,
"p95FrameDurationMs": 16.800000000000182
},
{
"name": "subgraph-transition-enter",
"durationMs": 1398.9009999999666,
"styleRecalcs": 18,
"styleRecalcDurationMs": 31.14,
"layouts": 14,
"layoutDurationMs": 12.926000000000004,
"taskDurationMs": 962.5430000000001,
"heapDeltaBytes": 8676044,
"heapUsedBytes": 97910764,
"domNodes": 13673,
"jsHeapTotalBytes": 10747904,
"scriptDurationMs": 33.452,
"eventListeners": 2373,
"totalBlockingTimeMs": 131,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "viewport-pan-sweep",
"durationMs": 9124.829000000034,
"styleRecalcs": 249,
"styleRecalcDurationMs": 39.489000000000004,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 5141.968999999999,
"heapDeltaBytes": 23228904,
"heapUsedBytes": 82982472,
"domNodes": -281,
"jsHeapTotalBytes": 7589888,
"scriptDurationMs": 1621.997,
"eventListeners": -129,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66333333333332,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "viewport-pan-sweep",
"durationMs": 8909.95799999996,
"styleRecalcs": 249,
"styleRecalcDurationMs": 39.629,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 5015.7480000000005,
"heapDeltaBytes": 20733176,
"heapUsedBytes": 80379100,
"domNodes": -281,
"jsHeapTotalBytes": 7589888,
"scriptDurationMs": 1567.835,
"eventListeners": -131,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.80000000000109
},
{
"name": "vue-large-graph-idle",
"durationMs": 17586.671999999966,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17568.388,
"heapDeltaBytes": -42645868,
"heapUsedBytes": 170797844,
"domNodes": -8312,
"jsHeapTotalBytes": -7806976,
"scriptDurationMs": 582.889,
"eventListeners": -16389,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.329999999999927,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "vue-large-graph-idle",
"durationMs": 17589.13099999995,
"styleRecalcs": 0,
"styleRecalcDurationMs": 0,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 17568.076999999997,
"heapDeltaBytes": -44907152,
"heapUsedBytes": 167259512,
"domNodes": -8313,
"jsHeapTotalBytes": -10428416,
"scriptDurationMs": 580.485,
"eventListeners": -16387,
"totalBlockingTimeMs": 0,
"frameDurationMs": 18.329999999999927,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 21941.78499999998,
"styleRecalcs": 159,
"styleRecalcDurationMs": 22.309999999999995,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 21916.449,
"heapDeltaBytes": -53125016,
"heapUsedBytes": 154464928,
"domNodes": -8312,
"jsHeapTotalBytes": -11739136,
"scriptDurationMs": 918.054,
"eventListeners": -16387,
"totalBlockingTimeMs": 578,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "vue-large-graph-pan",
"durationMs": 21297.486000000048,
"styleRecalcs": 147,
"styleRecalcDurationMs": 19.564999999999998,
"layouts": 0,
"layoutDurationMs": 0,
"taskDurationMs": 21249.085,
"heapDeltaBytes": -57288188,
"heapUsedBytes": 152455036,
"domNodes": -8316,
"jsHeapTotalBytes": -13049856,
"scriptDurationMs": 950.2409999999999,
"eventListeners": -16381,
"totalBlockingTimeMs": 409,
"frameDurationMs": 17.776666666666642,
"p95FrameDurationMs": 16.799999999999272
},
{
"name": "workflow-execution",
"durationMs": 470.1430000000073,
"styleRecalcs": 14,
"styleRecalcDurationMs": 25.032000000000004,
"layouts": 3,
"layoutDurationMs": 0.6350000000000001,
"taskDurationMs": 137.55800000000002,
"heapDeltaBytes": -15759532,
"heapUsedBytes": 51830724,
"domNodes": 134,
"jsHeapTotalBytes": 7454720,
"scriptDurationMs": 11.414999999999997,
"eventListeners": 99,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.666666666666668,
"p95FrameDurationMs": 16.700000000000728
},
{
"name": "workflow-execution",
"durationMs": 128.04300000004787,
"styleRecalcs": 9,
"styleRecalcDurationMs": 16.117,
"layouts": 2,
"layoutDurationMs": 1.34,
"taskDurationMs": 85.593,
"heapDeltaBytes": 3002136,
"heapUsedBytes": 70607588,
"domNodes": 129,
"jsHeapTotalBytes": 2883584,
"scriptDurationMs": 6.715000000000001,
"eventListeners": 25,
"totalBlockingTimeMs": 0,
"frameDurationMs": 16.66999999999998,
"p95FrameDurationMs": 16.799999999999727
}
]
} |
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## cloud/1.49 #15176 +/- ##
=============================================
Coverage ? 71.92%
=============================================
Files ? 1767
Lines ? 92461
Branches ? 24893
=============================================
Hits ? 66499
Misses ? 25843
Partials ? 119
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
Backport of #14501 to
cloud/1.49Automatically created by backport workflow.