Register Node / any RPC call fails with "Unexpected end of JSON input" when Compute is embedded in Hero OS #93
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_compute#93
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
The Compute dashboard loads inside Hero OS but any RPC call triggered from the UI (e.g. Register Node) fails immediately with:
Works fine on direct
http://127.0.0.1:9001— only broken throughhero_router(and therefore through the Hero OS shell).Environment
hero_compute:developmenthero_compute_ui: serves/rpcas a JSON-RPC proxy onto the compute server's Unix sockethero_compute_server: exposes JSON-RPC at/api/root/cloud/rpconhero_compute/rpc.sock(set byhero_compute_sdk::SERVER_RPC_PATH)hero_router: routes/{service}/{webname}/*and applies a "sibling shortcut" forPOST /<svc>/ui/rpc(seehero_router/crates/hero_router/src/server/routes.rs:954-990)Root cause
Hero router's sibling shortcut intentionally re-dispatches any
POST /<svc>/ui/rpc*to the service'srpc.sockat path/rpc*, bypassingui.sock(so admin UIs don't need their own RPC proxy).But
hero_compute_server'srpc.sockdoes not expose a root/rpcroute — only/api/root/cloud/rpc, because it usesAxumRpcServerwithapi_prefix: "/api"+register_app("root", "cloud", …).Result: the shortcut forwards
POST /rpcto a non-existent route onrpc.sock→404withcontent-length: 0→ browserr.json()throws "Unexpected end of JSON input".Reproduce
Fix options
Option 1 — UI-side workaround (small, local, unblocks Hero OS)
Rename the UI's RPC proxy route so it no longer matches the router's
/rpc*shortcut pattern, then update the JS to call the new path.crates/hero_compute_ui/src/server.rs: add.route("/api/rpc", post(rpc_proxy).options(cors_preflight))(and.route("/api/explorer/rpc", ...)for symmetry) alongside the existing/rpcroute.crates/hero_compute_ui/static/js/dashboard.js: changeu("/rpc")→u("/api/rpc")andu("/explorer/rpc")→u("/api/explorer/rpc").Through the router,
POST /hero_compute/ui/api/rpcnow goes: browser → router →ui.sock /api/rpc→ UI'srpc_proxy→ compute server's real/api/root/cloud/rpcendpoint. The sibling shortcut stays out of the way because the forward_path doesn't start with/rpc. Direct:9001access keeps working unchanged.Pros: single-repo change, ~4 lines, no impact on other services.
Cons: leaves the UI proxy in place (the shortcut was meant to make it unnecessary). Pattern diverges from the
hero_ui_openrpc_proxyskill.Option 2 — Server-side proper fix (aligns with Hero convention)
Make
hero_compute_server'srpc.sockexpose a root/rpcJSON-RPC endpoint (and matching/openrpc.json), so the router's sibling shortcut reaches a real handler. The UI's localrpc_proxythen becomes redundant and can be removed; JS callsu("/rpc")as before.Likely implementation paths:
AxumRpcServerwithapi_prefix: ""and register the domain under a single/rpcroute, OR/rpcalias that proxies into the existing/api/root/cloud/rpcdispatch, ORhero_rpc::AxumRpcServerupstream so every service automatically exposes a canonical/rpcregardless of tenant/app structure (biggest blast radius, fixes the whole ecosystem).Pros: removes a redundant proxy layer;
hero_computenow follows the same sibling-shortcut convention as other Hero services; UI code matches thehero_ui_openrpc_proxyskill.Cons: touches
hero_compute_server(and possiblyhero_rpc); requires care for multi-tenant/app path expectations inheartbeat_sender.rs,explorer/proxy.rs, OpenRPC specs, and integration tests (SERVER_RPC_PATHis referenced in several places).Recommendation
Ship Option 1 first as an immediate unblock for Hero OS users, then follow up with Option 2 as the architectural cleanup. Track both under this issue.
Acceptance
:9001access continues to work unchangedcurl --unix-socket rpc.sock http://localhost/rpcwith a valid JSON-RPC 2.0 payload returns 200 + JSONReferences
hero_router/crates/hero_router/src/server/routes.rs:954-990— sibling shortcut rewritehero_compute_sdk/src/lib.rs:83—SERVER_RPC_PATH = "/api/root/cloud/rpc"hero_compute_server/src/main.rs:90-122—AxumRpcServersetup withapi_prefix: "/api"+register_app("root", "cloud", …)hero_compute_ui/src/server.rs:197— UI's/rpcproxy routehero_compute_ui/static/js/dashboard.js:124—rpc()helperhero_ui_openrpc_proxy/hero_web_prefixskills — standard patterns/hero_compute/uiprefix fix)Implementation Spec — Issue #93 (Option 1)
Objective
Add new UI proxy routes
/api/rpcand/api/explorer/rpcalongside the existing/rpcand/explorer/rpcroutes inhero_compute_ui, and update the dashboard JavaScript to call the new paths. This preventshero_router's sibling-shortcut logic (which re-dispatches anyPOST /<svc>/ui/rpc*to the service'srpc.sockat path/rpc*) from bypassingui.sockand hitting a non-existent root/rpcendpoint onhero_compute_server. Because the router's shortcut matches the prefix/rpc, forwarding via/api/rpcsidesteps it entirely.This spec covers Option 1 (UI-side workaround) only. Option 2 (server-side fix adding a root
/rpcroute tohero_compute_server) is out of scope and will be addressed in a separate follow-up issue.Requirements
/api/rpcand/api/explorer/rpcMUST be added to the UI Axum router and point to the existingrpc_proxy/explorer_rpc_proxyhandlers with CORS preflight support, identical to the existing routes./rpc,/hero_compute/rpc, and/explorer/rpcMUST remain in place so directhttp://127.0.0.1:9001access and external callers keep working unchanged.rpc()andexplorerRpc()) MUST call the new/api/rpcand/api/explorer/rpcpaths so that every template consumer migrates in one change.sendRpcCall()(OpenRPC explorer page) MUST be updated to the new paths.POST /hero_compute/ui/api/rpcmust reach: browser -> router ->ui.sock /api/rpc->rpc_proxy-> compute server/api/root/cloud/rpc. Because the router's sibling-shortcut pattern only triggers onui/rpc*, a request path starting withui/api/rpcavoids the shortcut.hero_compute_server) changes; nohero_compute_sdkconstant changes; no template changes.Files to modify
crates/hero_compute_ui/src/server.rs— add two new route bindings inside theRouter::new()builder chain inrun_server(), alongside the existing RPC proxy routes. Keep existing routes untouched.crates/hero_compute_ui/static/js/dashboard.js— update three call sites: therpc()helper (line ~127), theexplorerRpc()helper (line ~128), and the hardcoded URL insidesendRpcCall()(line ~2170, OpenRPC explorer "Try it" button).No template (
*.html) files need to change — every template uses the two helpers indashboard.js.Implementation Plan
Step 1 — Add new proxy routes in
server.rsFiles:
crates/hero_compute_ui/src/server.rsDependencies: none (parallel with Step 2).
In the
Router::new()chain insiderun_server(), locate the comment// RPC proxy routes (kept from original)(currently around line 254). Immediately after the existing three route bindings:add two additional bindings that reuse the same handlers:
Why:
hero_router's sibling-shortcut intercepts anyPOST /<svc>/ui/rpc*and re-dispatches it to the service'srpc.sockat path/rpc*. Sincehero_compute_serverdoes not expose a root/rpc(it serves under/api/root/cloud/rpc), the shortcut causes a 404 with empty body and the browser'sr.json()throwsUnexpected end of JSON input. A route whose path does not start with/rpcis not intercepted, so/api/rpcand/api/explorer/rpcreach the UI proxy intact.Verification:
cargo build -p hero_compute_uisucceeds and the existing inline tests still pass.Step 2 — Point the JS helpers at the new paths
Files:
crates/hero_compute_ui/static/js/dashboard.jsDependencies: none (parallel with Step 1; ship Steps 1 and 2 together).
Three edits:
rpc()helper — changeHERO_PREFIX + "/rpc"toHERO_PREFIX + "/api/rpc".explorerRpc()helper — changeHERO_PREFIX + "/explorer/rpc"toHERO_PREFIX + "/api/explorer/rpc".sendRpcCall()— change the ternary URL fromHERO_PREFIX + "/explorer/rpc" : HERO_PREFIX + "/rpc"toHERO_PREFIX + "/api/explorer/rpc" : HERO_PREFIX + "/api/rpc".Why: These three call sites are the only places in the UI layer that hit the
/rpcand/explorer/rpcpaths. All templates (index.html,nodes.html,vms.html,admin.html,explorer.html) go throughrpc()/explorerRpc(), so updating these helpers migrates every consumer at once.sendRpcCall()is the OpenRPC playground's directfetchand must be migrated separately.Verification: grep the repo to confirm no remaining
HERO_PREFIX + "/rpc"orHERO_PREFIX + "/explorer/rpc"literals exist indashboard.js; only the new/api/rpcand/api/explorer/rpcpaths should appear.Step 3 — Smoke check (post-rebuild)
Files: none (runtime verification only).
Dependencies: Steps 1 and 2 complete;
hero_compute_uirebuilt and restarted.POST http://127.0.0.1:9001/api/rpcwith a valid JSON-RPC 2.0 body returns 200 +application/json.POST http://127.0.0.1:9001/rpcstill returns a valid response.hero_router: load the compute dashboard inside Hero OS; "Register Node" must complete successfully with noUnexpected end of JSON inputin the console./explorermust execute method calls against both server and explorer OpenRPC specs.Acceptance Criteria
server.rsexposesPOST /api/rpcandOPTIONS /api/rpcwired torpc_proxy/cors_preflight.server.rsexposesPOST /api/explorer/rpcandOPTIONS /api/explorer/rpcwired toexplorer_rpc_proxy/cors_preflight.server.rsstill exposes legacyPOST /rpc,POST /hero_compute/rpc, andPOST /explorer/rpcroutes unchanged.dashboard.jsrpc()helper targetsHERO_PREFIX + "/api/rpc".dashboard.jsexplorerRpc()helper targetsHERO_PREFIX + "/api/explorer/rpc".dashboard.jssendRpcCall()targets/api/rpcand/api/explorer/rpc.http://127.0.0.1:9001/api/rpcreturns a valid JSON-RPC response.http://127.0.0.1:9001/rpc(legacy) still returns a valid JSON-RPC response.hero_router/ Hero OS, "Register Node" succeeds withoutUnexpected end of JSON input.serverandexplorerspecs.cargo build -p hero_compute_uisucceeds; existing inline tests inserver.rsstill pass.Notes
/rpcroute tohero_compute_server) will be filed as a separate follow-up.hero_compute_server, nohero_compute_sdk, no template changes.Test Results
Command:
cargo test -p hero_compute_uiWorkspace build:
cargo buildPASSAll tests passed.
Note: Option 1 is a UI-side change (two new Axum routes reusing existing handlers, plus three JS URL updates). JavaScript code paths are not covered by Rust tests — they're covered by the manual smoke checks in the spec (Step 3). The route additions in server.rs are covered implicitly by successful compilation of the Router::new() chain; the handlers themselves (rpc_proxy, explorer_rpc_proxy, cors_preflight) are untouched.
Summary
Implemented Option 1 (UI-side workaround) for issue #93. When
hero_compute_uiis embedded behindhero_routerinside Hero OS, the router's sibling-shortcut matches any path starting with/rpc, which caused RPC calls (including Register Node) to fail with "Unexpected end of JSON input". The fix adds new/api/rpcand/api/explorer/rpcproxy routes inhero_compute_uithat the router does not intercept, and updates the dashboard JavaScript to target them. The existing/rpc,/hero_compute/rpc, and/explorer/rpcroutes are left in place for backward compatibility.Files changed
crates/hero_compute_ui/src/server.rs— added.route("/api/rpc", post(rpc_proxy).options(cors_preflight))and.route("/api/explorer/rpc", post(explorer_rpc_proxy).options(cors_preflight))to theRouter::new()chain inrun_server(); handlers (rpc_proxy,explorer_rpc_proxy,cors_preflight) are unchanged and reused by the new routes.crates/hero_compute_ui/static/js/dashboard.js— updated three call sites to use the new paths:rpc()helper now callsHERO_PREFIX + "/api/rpc",explorerRpc()helper now callsHERO_PREFIX + "/api/explorer/rpc", and thesendRpcCall()ternary now selects betweenHERO_PREFIX + "/api/explorer/rpc"andHERO_PREFIX + "/api/rpc". Grep post-checks confirm no remainingHERO_PREFIX + "/rpc"orHERO_PREFIX + "/explorer/rpc"literals.Test results
cargo test -p hero_compute_ui: 8 total, 8 passed, 0 failed (allsanitize_base_pathvariants, middleware header handling, andtest_normalize_socket_path).cargo build(workspace): PASS.Smoke checks pending
After deploy, the following runtime checks must be verified:
http://127.0.0.1:9001/api/rpc— should reach the UI proxy and return a valid JSON-RPC response (no "Unexpected end of JSON input").http://127.0.0.1:9001/rpc— should still work when calling the UI directly (backward compatibility preserved).hero_routerfrom inside Hero OS — Register Node and other RPC calls from the embedded dashboard must succeed without being intercepted by the router's/rpc*sibling-shortcut.Notes
This change implements Option 1 (UI-side workaround) only. Option 2 (server-side fix in
hero_routerto narrow the/rpc*sibling-shortcut) remains as a potential follow-up.