embedder UI fails silently when backend unavailable: rpcCall and panels both swallow errors #20
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_embedder#20
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
When the embedder backend is unreachable, the Embedder admin dashboard becomes silently unusable: every panel that hits the JSON-RPC backend gets stuck on "Loading..." forever, and the only error surfaces is a cryptic
Unexpected token 'S', "Socket 'rp"... is not valid JSONin the JS console. There is no on-screen indication of the actual problem.This is the combination of two defects in
hero_embedder_uithat together produce the broken UX:rpcCall()blind-parses the response as JSON — noresponse.ok/Content-Typecheck beforeawait response.json(). Whenhero_routerreturnstext/plain(e.g. a 404 with bodySocket 'rpc.sock' not found for 'hero_embedder'),JSON.parsethrows "Unexpected token" and the actual error context is lost.catchblock callsconsole.error(...)and never updates the UI, so the user sees a perpetual "Loading..." with no indication of what went wrong.Both need to be fixed for the dashboard to fail gracefully when the backend is down.
Reproduction
hero_embedder_servernot running (so~/hero/var/sockets/hero_embedder/rpc.sockdoes not exist), open the Embedder dashboard.Unexpected token 'S', "Socket 'rp"... is not valid JSON.Root cause — Part 1: blind JSON parse in
rpcCallcrates/hero_embedder_ui/templates/base.html:177-219(rpcCall):There is no validation of
response.okor theContent-Typeheader before parsing. Any non-JSON 4xx/5xx body —hero_router's 404 text/plain when the upstream socket is missing, future 502s from any proxy, gzip-encoded HTML error pages — surfaces as "Unexpected token" and loses the real error.Root cause — Part 2: silent panel-level error handling
Each panel's poller wraps
rpcCallintry/catchbut onlyconsole.errors on failure. Affected sites:crates/hero_embedder_ui/templates/components/stats_card.html~lines 175-186 (pollServerStats)crates/hero_embedder_ui/templates/components/stats_card.html~lines 197-216 (pollNamespaceStats)crates/hero_embedder_ui/templates/base.html~lines 133-175 (loadNamespaces)crates/hero_embedder_ui/templates/fragments/loader_pane.html~lines 193-218 (loader refresh)crates/hero_embedder_ui/templates/fragments/logs_pane.html~lines 102-120 (logs poll)Pattern is identical across all of them:
Suggested fix direction
Part 1 —
rpcCall(single function inbase.html)Validate the response shape before parsing, and throw an error message that contains the response body (truncated) when the response is non-JSON or non-OK:
Part 2 — panel-level error rendering
Add a small shared helper in
base.html:Replace each silent
console.errorwithrenderPanelError("<panel-id>", err.message)(keepconsole.errortoo if useful for dev). For the namespace<select>, swap the inner content with a single disabled option whose text is the error.When the next poll succeeds, the panel render path naturally overwrites the error state — no extra recovery code needed.
Acceptance criteria
rpcCallno longer throws "Unexpected token" when the response body is not JSON; instead it throws a meaningfulErrorcontaining the HTTP status and a snippet of the response body.rpcCallbehaviour is unchanged (no regressions in the success path).Notes
Socket 'rpc.sock' not found for 'hero_embedder'body itself is emitted byhero_router(crates/hero_router/src/server/routes.rs:1373-1377in thehero_routerrepo) for non-HTML clients. That router-side behaviour is debatable separately, but this UI-side bug stands on its own — the dashboard must not break when the upstream returns any non-JSON body, regardless of whathero_routerchooses to send.rpcCalldefensive pattern is reusable in other Hero*_uidashboards; a quick scan suggestshero_browser_ui'srpc()helper has the same issue. Out of scope for this issue but worth porting later.Implementation Spec for Issue #20
Objective
Make the
hero_embedder_uidashboard fail loudly and recoverably whenhero_embedder_server(or any upstreamrpc.sock) is unavailable. Two coordinated changes: (1)rpcCall()inbase.htmlmust validate the HTTP response before parsing as JSON, so non-JSON 4xx/5xx replies (e.g.hero_router'stext/plain404Socket 'rpc.sock' not found for 'hero_embedder') surface as readable error messages instead of "Unexpected token 'S'…" parse failures; (2) every panel-level poller stops swallowing errors withconsole.errorand renders a visible inline alert (or, for the namespace dropdown, a disabled error option) that is automatically cleared by the next successful poll.Files to Modify/Create
crates/hero_embedder_ui/templates/base.html— fixrpcCall(response validation), add sharedescapeHtmlhelper, add sharedrenderPanelErrorhelper, replaceloadNamespacesconsole.errorwith disabled-option error state on the<select>.crates/hero_embedder_ui/templates/components/stats_card.html— replace silentconsole.errorinpollServerStatsandpollNamespaceStatswithrenderPanelErrorcalls into the existing panel containers.crates/hero_embedder_ui/templates/fragments/loader_pane.html— add a stable error container in the loader card; replace silentconsole.errorinrefreshLoaderState(and the secondaryindex.clear.catch) with arenderPanelErrorcall against it.crates/hero_embedder_ui/templates/fragments/logs_pane.html— replace silentconsole.errorinLogsManager.loadLogsandclearLogswithrenderPanelErrorcalls intologs-content.No new files. No backend Rust changes.
Helper specifications (placed in
base.html, same<script>block asrpcCall)rpcCallrewrite (drop intobase.htmllines 177–219)Insert immediately before the existing
await response.json():Keep the existing
data.errorcheck,data.resultreturn, response-time DOM update, and AbortError handling unchanged.Implementation Plan
Step 1: base.html — rpcCall validation, helpers, namespace dropdown error state
Files:
crates/hero_embedder_ui/templates/base.htmlDependencies: none
<script>block that begins at line 94, addescapeHtmlandrenderPanelErrordefinitions beforerpcCall(so all later code in the file and all included panel files can use them).rpcCall(lines 177–219) to perform theresponse.ok+Content-Typevalidation described above beforeawait response.json().loadNamespaces(lines 133–175). Replace theconsole.error-only catch with: clearselect.innerHTML, append one<option value="" disabled selected>whosetextContentis"Backend unavailable: " + e.message, and setselect.disabled = true. On the success path, addselect.disabled = falsebefore the existingselect.innerHTML = "".Step 2: stats_card.html — Server Stats and Namespace Stats panels
Files:
crates/hero_embedder_ui/templates/components/stats_card.htmlDependencies: Step 1
pollServerStats(~lines 175–186): replaceconsole.error("[ServerStats] Poll error:", err);withrenderPanelError("server-stats-content", "Server stats unavailable: " + err.message);. Recovery is automatic — the success path already doescontainer.outerHTML = renderServerStats(...)which re-creates#server-stats-content.pollNamespaceStats(~lines 197–216): replace theconsole.errorwithrenderPanelError("namespace-stats-area", "Namespace stats unavailable: " + err.message);. Recovery is automatic via the existingcontainer.innerHTML = renderNamespaceStats(data)on success.Step 3: loader_pane.html — Corpus Loader pane
Files:
crates/hero_embedder_ui/templates/fragments/loader_pane.htmlDependencies: Step 1
<div id="load-progress" class="mt-2"></div>(~line 63):<div id="loader-error" class="mt-2"></div>.refreshLoaderState(~lines 193–218): at the top of thetryblock, clear the error viaconst errEl = document.getElementById("loader-error"); if (errEl) errEl.innerHTML = "";so a successful refresh wipes any previous error. In thecatch, replace theconsole.errorwithrenderPanelError("loader-error", "Loader unavailable: " + e.message);.console.errorwithrenderPanelError("loader-error", "Failed to clear namespace: " + e.message);.alert alert-dangerblock in#load-progress.Step 4: logs_pane.html — Logs tab
Files:
crates/hero_embedder_ui/templates/fragments/logs_pane.htmlDependencies: Step 1
LogsManager.loadLogs(~lines 102–120): replaceconsole.error("[LogsManager] Error loading logs:", error);withrenderPanelError("logs-content", "Logs unavailable: " + error.message);. Recovery is automatic —renderLogs()always callscontainer.innerHTML = ...on success.LogsManager.clearLogs(~lines 197–206): replace theconsole.errorwithrenderPanelError("logs-content", "Failed to clear logs: " + error.message);.Parallelization summary
Step 1 must complete first because Steps 2/3/4 all call
renderPanelError. Once Step 1 is done, Steps 2, 3, and 4 are fully independent (different files, no shared symbols) and can be implemented in parallel.Acceptance Criteria
rpcCallno longer throwsSyntaxError: Unexpected token 'S', "Socket 'rp"...when the upstream returnstext/plain404. Instead it throwsError("Backend unavailable (HTTP 404): Socket 'rpc.sock' not found for 'hero_embedder'").{"jsonrpc":"2.0","error":{...}}),rpcCallbehaviour is unchanged.hero_embedder_serverstopped, the Server Stats card replaces its "Loading..." with a yellowalert alert-warningreading the upstream error message.hero_embedder_serverstopped, the Namespace Stats card replaces its "Loading..." with the same alert wording.hero_embedder_serverstopped, the namespace dropdown shows one disabledBackend unavailable: …option and is itself disabled.hero_embedder_serverstopped, the Loader pane shows the alert in#loader-errorbelow the load button; the rest of the loader UI remains rendered.hero_embedder_serverstopped, the Logs tab body shows the alert in place of "Loading logs..." or any previously rendered logs.hero_embedder_serveris restarted, every panel and the dropdown recover on the next poll tick without a page refresh.escapeHtml(XSS-safe even if the upstream body contains HTML/quotes); the dropdown error usestextContentand is therefore also safe.Manual verification plan
cargo build -p hero_embedder_ui(orservice_embedder start --resetto also rebuild and re-register).hero_embedder_serverrunning, open the dashboard and confirm the happy path works (Server Stats, Namespace Stats, Loader pane, Logs tab, namespace dropdown all populate).hero_proc service stop hero_embedder_server(or analogous). Within ~10 s confirm in the browser:SyntaxError: Unexpected tokenin DevTools console./rpcPOSTs returning404 text/plainwith bodySocket 'rpc.sock' not found for 'hero_embedder'.Backend unavailable: …option.Notes / Out of scope
hero_embedderdONNX dylib failures andhero_embedder_servermissing models: separate concerns, not addressed here.hero_router'stext/plain404 emission: filed separately. Our fix is defense-in-depth so the UI no longer relies on the upstream returning JSON.hero_embedder_proxy::openrpc_handler's{}swallow: tracked separately as #21.rpcCallvalidation tohero_browser_ui'srpc()helper: out of scope for this branch.Test Results & Implementation Summary
Branch:
development_ui_error_handling(offorigin/development).Build / static check
cargo check --workspace --bins— clean across all 5 crates (hero_embedder,hero_embedderd,hero_embedder_proxy,hero_embedder_server,hero_embedder_ui).End-to-end manual verification (browser)
Setup: with
hero_embedder_serverfailed (no models on this machine — environmental, out of scope),rpc.sockdoes not exist.hero_routerreturns404 text/plainwith bodySocket 'rpc.sock' not found for 'hero_embedder'for everyPOST /hero_embedder/ui/rpc. This is the exact failure mode the issue targets.Driving the dashboard at
http://127.0.0.1:9151/hero_embedder/ui/headlessly:rpcCallSyntaxError: Unexpected token 'S', "Socket 'rp"... is not valid JSONError("Backend unavailable (HTTP 404): Socket 'rpc.sock' not found for 'hero_embedder'")alert alert-warningwith the upstream errorNamespaces unavailable: ...#loader-error#logs-contentSyntaxErrorsmcp__hero_browser__console_messagesreturned[])Diff summary
Changes
templates/base.htmlescapeHtml(s)helper.renderPanelError(containerId, message)helper (Bootstrapalert alert-warning, XSS-safe viaescapeHtml).rpcCall: when response is non-OK and notapplication/json, read the body as text and throwError("Backend unavailable (HTTP <status>): <body[:200]>"). OK and JSON-RPC-error paths unchanged.loadNamespaces: success path addsselect.disabled = falsebefore re-populating; catch path appends one disabled<option>whosetextContentis"Namespaces unavailable: " + e.messageand setsselect.disabled = true. UsestextContent(no escaping needed).templates/components/stats_card.html—pollServerStatsandpollNamespaceStatsconsole.error→renderPanelError("server-stats-content", ...)/renderPanelError("namespace-stats-area", ...). Recovery automatic via existing success-pathouterHTML/innerHTMLrewrites.templates/fragments/loader_pane.html— added<div id="loader-error" class="mt-2"></div>after#load-progress.refreshLoaderStateclears#loader-erroron entry; catch renders into it. Reset-button.catchalso routes to#loader-error.templates/fragments/logs_pane.html—LogsManager.loadLogsandLogsManager.clearLogsconsole.error→renderPanelError("logs-content", ...).Acceptance criteria — all met
rpcCallno longer throws "Unexpected token" ontext/plain404; throwsError("Backend unavailable (HTTP 404): ...")instead.Namespaces unavailable: ...option.#loader-error; rest of loader UI remains rendered.innerHTML/outerHTMLsuccess paths and#loader-errorclearing on entry torefreshLoaderState.escapeHtml(XSS-safe); dropdown usestextContent.Notes
hero_embedder_servercontinues to fail (missing models — environmental, out of scope per the issue). The recovery path is structurally guaranteed: every panel's success-path overwrites the same DOM container thatrenderPanelErrorwrites into;loadNamespacesre-enables the<select>on success;#loader-erroris cleared at the top of everyrefreshLoaderStatetry."Backend unavailable: "to"Namespaces unavailable: "to avoid the redundant"Backend unavailable: Backend unavailable (HTTP 404)..."reading. All other panel prefixes (Server stats / Namespace stats / Loader / Logs unavailable:) read cleanly with the underlyingBackend unavailable (HTTP 404): ...message.hero_embedderdONNX dylib failures,hero_embedder_servermissing models,hero_routertext/plain404 emission (separate concern),hero_embedder_proxy::openrpc_handler{}swallow (tracked separately as #21), porting the samerpcCallvalidation tohero_browser_ui.