Register Node / any RPC call fails with "Unexpected end of JSON input" when Compute is embedded in Hero OS #93

Closed
opened 2026-04-16 15:29:45 +00:00 by rawan · 3 comments
Member

Summary

The Compute dashboard loads inside Hero OS but any RPC call triggered from the UI (e.g. Register Node) fails immediately with:

ERROR: Failed to execute 'json' on 'Response': Unexpected end of JSON input

Works fine on direct http://127.0.0.1:9001 — only broken through hero_router (and therefore through the Hero OS shell).

Environment

  • hero_compute: development
  • hero_compute_ui: serves /rpc as a JSON-RPC proxy onto the compute server's Unix socket
  • hero_compute_server: exposes JSON-RPC at /api/root/cloud/rpc on hero_compute/rpc.sock (set by hero_compute_sdk::SERVER_RPC_PATH)
  • hero_router: routes /{service}/{webname}/* and applies a "sibling shortcut" for POST /<svc>/ui/rpc (see hero_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's rpc.sock at path /rpc*, bypassing ui.sock (so admin UIs don't need their own RPC proxy).

But hero_compute_server's rpc.sock does not expose a root /rpc route — only /api/root/cloud/rpc, because it uses AxumRpcServer with api_prefix: "/api" + register_app("root", "cloud", …).

Result: the shortcut forwards POST /rpc to a non-existent route on rpc.sock404 with content-length: 0 → browser r.json() throws "Unexpected end of JSON input".

Reproduce

# Direct — works
curl -X POST http://127.0.0.1:9001/rpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"computeservice.node_register","params":{}}'
# → 200 OK, JSON body

# Through router with `/ui/rpc` sibling shortcut — fails
curl -X POST http://127.0.0.1:9988/hero_compute/ui/rpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"computeservice.node_register","params":{}}'
# → 404, content-length: 0

# Through router with the full real path — works
curl -X POST http://127.0.0.1:9988/hero_compute/rpc/api/root/cloud/rpc \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"computeservice.node_register","params":{}}'
# → 200 OK, JSON body

# Probing rpc.sock directly confirms the missing `/rpc` route
curl --unix-socket ~/hero/var/sockets/hero_compute/rpc.sock \
     -X POST http://localhost/rpc -d '{...}'
# → 404

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 /rpc route.
  • crates/hero_compute_ui/static/js/dashboard.js: change u("/rpc")u("/api/rpc") and u("/explorer/rpc")u("/api/explorer/rpc").

Through the router, POST /hero_compute/ui/api/rpc now goes: browser → router → ui.sock /api/rpc → UI's rpc_proxy → compute server's real /api/root/cloud/rpc endpoint. The sibling shortcut stays out of the way because the forward_path doesn't start with /rpc. Direct :9001 access 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_proxy skill.

Option 2 — Server-side proper fix (aligns with Hero convention)

Make hero_compute_server's rpc.sock expose a root /rpc JSON-RPC endpoint (and matching /openrpc.json), so the router's sibling shortcut reaches a real handler. The UI's local rpc_proxy then becomes redundant and can be removed; JS calls u("/rpc") as before.

Likely implementation paths:

  • Configure AxumRpcServer with api_prefix: "" and register the domain under a single /rpc route, OR
  • Add a root /rpc alias that proxies into the existing /api/root/cloud/rpc dispatch, OR
  • Fix hero_rpc::AxumRpcServer upstream so every service automatically exposes a canonical /rpc regardless of tenant/app structure (biggest blast radius, fixes the whole ecosystem).

Pros: removes a redundant proxy layer; hero_compute now follows the same sibling-shortcut convention as other Hero services; UI code matches the hero_ui_openrpc_proxy skill.
Cons: touches hero_compute_server (and possibly hero_rpc); requires care for multi-tenant/app path expectations in heartbeat_sender.rs, explorer/proxy.rs, OpenRPC specs, and integration tests (SERVER_RPC_PATH is 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

  • Register Node works from inside Hero OS (Compute island iframe)
  • All other RPC-driven actions in the UI (list VMs, deploy, start/stop/delete, node stats, SSH key mgmt, API Explorer "Run", etc.) work through the router
  • Direct :9001 access continues to work unchanged
  • For Option 2: curl --unix-socket rpc.sock http://localhost/rpc with a valid JSON-RPC 2.0 payload returns 200 + JSON
  • No empty-body responses on the RPC path

References

  • hero_router/crates/hero_router/src/server/routes.rs:954-990 — sibling shortcut rewrite
  • hero_compute_sdk/src/lib.rs:83SERVER_RPC_PATH = "/api/root/cloud/rpc"
  • hero_compute_server/src/main.rs:90-122AxumRpcServer setup with api_prefix: "/api" + register_app("root", "cloud", …)
  • hero_compute_ui/src/server.rs:197 — UI's /rpc proxy route
  • hero_compute_ui/static/js/dashboard.js:124rpc() helper
  • hero_ui_openrpc_proxy / hero_web_prefix skills — standard patterns
  • Related: first issue (/hero_compute/ui prefix fix)

image

## Summary The Compute dashboard loads inside Hero OS but any RPC call triggered from the UI (e.g. **Register Node**) fails immediately with: ``` ERROR: Failed to execute 'json' on 'Response': Unexpected end of JSON input ``` Works fine on direct `http://127.0.0.1:9001` — only broken through `hero_router` (and therefore through the Hero OS shell). ## Environment - `hero_compute`: `development` - `hero_compute_ui`: serves `/rpc` as a JSON-RPC proxy onto the compute server's Unix socket - `hero_compute_server`: exposes JSON-RPC at `/api/root/cloud/rpc` on `hero_compute/rpc.sock` (set by `hero_compute_sdk::SERVER_RPC_PATH`) - `hero_router`: routes `/{service}/{webname}/*` and applies a "sibling shortcut" for `POST /<svc>/ui/rpc` (see `hero_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's `rpc.sock` at path `/rpc*`, bypassing `ui.sock` (so admin UIs don't need their own RPC proxy). But `hero_compute_server`'s `rpc.sock` does not expose a root `/rpc` route — only `/api/root/cloud/rpc`, because it uses `AxumRpcServer` with `api_prefix: "/api"` + `register_app("root", "cloud", …)`. Result: the shortcut forwards `POST /rpc` to a non-existent route on `rpc.sock` → `404` with `content-length: 0` → browser `r.json()` throws *"Unexpected end of JSON input"*. ## Reproduce ```bash # Direct — works curl -X POST http://127.0.0.1:9001/rpc \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"computeservice.node_register","params":{}}' # → 200 OK, JSON body # Through router with `/ui/rpc` sibling shortcut — fails curl -X POST http://127.0.0.1:9988/hero_compute/ui/rpc \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"computeservice.node_register","params":{}}' # → 404, content-length: 0 # Through router with the full real path — works curl -X POST http://127.0.0.1:9988/hero_compute/rpc/api/root/cloud/rpc \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"computeservice.node_register","params":{}}' # → 200 OK, JSON body # Probing rpc.sock directly confirms the missing `/rpc` route curl --unix-socket ~/hero/var/sockets/hero_compute/rpc.sock \ -X POST http://localhost/rpc -d '{...}' # → 404 ``` ## 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 `/rpc` route. - `crates/hero_compute_ui/static/js/dashboard.js`: change `u("/rpc")` → `u("/api/rpc")` and `u("/explorer/rpc")` → `u("/api/explorer/rpc")`. Through the router, `POST /hero_compute/ui/api/rpc` now goes: browser → router → `ui.sock /api/rpc` → UI's `rpc_proxy` → compute server's real `/api/root/cloud/rpc` endpoint. The sibling shortcut stays out of the way because the forward_path doesn't start with `/rpc`. Direct `:9001` access 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_proxy` skill. ### Option 2 — Server-side proper fix (aligns with Hero convention) Make `hero_compute_server`'s `rpc.sock` expose a root `/rpc` JSON-RPC endpoint (and matching `/openrpc.json`), so the router's sibling shortcut reaches a real handler. The UI's local `rpc_proxy` then becomes redundant and can be removed; JS calls `u("/rpc")` as before. Likely implementation paths: - Configure `AxumRpcServer` with `api_prefix: ""` and register the domain under a single `/rpc` route, **OR** - Add a root `/rpc` alias that proxies into the existing `/api/root/cloud/rpc` dispatch, **OR** - Fix `hero_rpc::AxumRpcServer` upstream so every service automatically exposes a canonical `/rpc` regardless of tenant/app structure (biggest blast radius, fixes the whole ecosystem). **Pros:** removes a redundant proxy layer; `hero_compute` now follows the same sibling-shortcut convention as other Hero services; UI code matches the `hero_ui_openrpc_proxy` skill. **Cons:** touches `hero_compute_server` (and possibly `hero_rpc`); requires care for multi-tenant/app path expectations in `heartbeat_sender.rs`, `explorer/proxy.rs`, OpenRPC specs, and integration tests (`SERVER_RPC_PATH` is 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 - [ ] **Register Node** works from inside Hero OS (Compute island iframe) - [ ] All other RPC-driven actions in the UI (list VMs, deploy, start/stop/delete, node stats, SSH key mgmt, API Explorer "Run", etc.) work through the router - [ ] Direct `:9001` access continues to work unchanged - [ ] For Option 2: `curl --unix-socket rpc.sock http://localhost/rpc` with a valid JSON-RPC 2.0 payload returns 200 + JSON - [ ] No empty-body responses on the RPC path ## References - `hero_router/crates/hero_router/src/server/routes.rs:954-990` — sibling shortcut rewrite - `hero_compute_sdk/src/lib.rs:83` — `SERVER_RPC_PATH = "/api/root/cloud/rpc"` - `hero_compute_server/src/main.rs:90-122` — `AxumRpcServer` setup with `api_prefix: "/api"` + `register_app("root", "cloud", …)` - `hero_compute_ui/src/server.rs:197` — UI's `/rpc` proxy route - `hero_compute_ui/static/js/dashboard.js:124` — `rpc()` helper - `hero_ui_openrpc_proxy` / `hero_web_prefix` skills — standard patterns - Related: first issue (`/hero_compute/ui` prefix fix) ![image](/attachments/64770f9f-2de7-4f03-b880-94aa49acf837)
rawan self-assigned this 2026-04-20 08:02:36 +00:00
Author
Member

Implementation Spec — Issue #93 (Option 1)

Objective

Add new UI proxy routes /api/rpc and /api/explorer/rpc alongside the existing /rpc and /explorer/rpc routes in hero_compute_ui, and update the dashboard JavaScript to call the new paths. This prevents hero_router's sibling-shortcut logic (which re-dispatches any POST /<svc>/ui/rpc* to the service's rpc.sock at path /rpc*) from bypassing ui.sock and hitting a non-existent root /rpc endpoint on hero_compute_server. Because the router's shortcut matches the prefix /rpc, forwarding via /api/rpc sidesteps it entirely.

This spec covers Option 1 (UI-side workaround) only. Option 2 (server-side fix adding a root /rpc route to hero_compute_server) is out of scope and will be addressed in a separate follow-up issue.

Requirements

  • New routes /api/rpc and /api/explorer/rpc MUST be added to the UI Axum router and point to the existing rpc_proxy / explorer_rpc_proxy handlers with CORS preflight support, identical to the existing routes.
  • Old routes /rpc, /hero_compute/rpc, and /explorer/rpc MUST remain in place so direct http://127.0.0.1:9001 access and external callers keep working unchanged.
  • The dashboard JavaScript helpers (rpc() and explorerRpc()) MUST call the new /api/rpc and /api/explorer/rpc paths so that every template consumer migrates in one change.
  • The hardcoded fetch URL inside sendRpcCall() (OpenRPC explorer page) MUST be updated to the new paths.
  • Through the router, POST /hero_compute/ui/api/rpc must reach: browser -> router -> ui.sock /api/rpc -> rpc_proxy -> compute server /api/root/cloud/rpc. Because the router's sibling-shortcut pattern only triggers on ui/rpc*, a request path starting with ui/api/rpc avoids the shortcut.
  • No changes to the RPC proxy handler internals — only new route bindings are added.
  • No server-side (hero_compute_server) changes; no hero_compute_sdk constant changes; no template changes.

Files to modify

  • crates/hero_compute_ui/src/server.rs — add two new route bindings inside the Router::new() builder chain in run_server(), alongside the existing RPC proxy routes. Keep existing routes untouched.
  • crates/hero_compute_ui/static/js/dashboard.js — update three call sites: the rpc() helper (line ~127), the explorerRpc() helper (line ~128), and the hardcoded URL inside sendRpcCall() (line ~2170, OpenRPC explorer "Try it" button).

No template (*.html) files need to change — every template uses the two helpers in dashboard.js.

Implementation Plan

Step 1 — Add new proxy routes in server.rs

Files: crates/hero_compute_ui/src/server.rs
Dependencies: none (parallel with Step 2).

In the Router::new() chain inside run_server(), locate the comment // RPC proxy routes (kept from original) (currently around line 254). Immediately after the existing three route bindings:

.route("/rpc", post(rpc_proxy).options(cors_preflight))
.route("/hero_compute/rpc", post(rpc_proxy).options(cors_preflight))
.route(
    "/explorer/rpc",
    post(explorer_rpc_proxy).options(cors_preflight),
)

add two additional bindings that reuse the same handlers:

// New router-safe paths: bypass hero_router's /rpc* sibling-shortcut (issue #93)
.route("/api/rpc", post(rpc_proxy).options(cors_preflight))
.route(
    "/api/explorer/rpc",
    post(explorer_rpc_proxy).options(cors_preflight),
)

Why: hero_router's sibling-shortcut intercepts any POST /<svc>/ui/rpc* and re-dispatches it to the service's rpc.sock at path /rpc*. Since hero_compute_server does not expose a root /rpc (it serves under /api/root/cloud/rpc), the shortcut causes a 404 with empty body and the browser's r.json() throws Unexpected end of JSON input. A route whose path does not start with /rpc is not intercepted, so /api/rpc and /api/explorer/rpc reach the UI proxy intact.

Verification: cargo build -p hero_compute_ui succeeds 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.js
Dependencies: none (parallel with Step 1; ship Steps 1 and 2 together).

Three edits:

  1. Line ~127, in the rpc() helper — change HERO_PREFIX + "/rpc" to HERO_PREFIX + "/api/rpc".
  2. Line ~128, in the explorerRpc() helper — change HERO_PREFIX + "/explorer/rpc" to HERO_PREFIX + "/api/explorer/rpc".
  3. Line ~2170, inside sendRpcCall() — change the ternary URL from HERO_PREFIX + "/explorer/rpc" : HERO_PREFIX + "/rpc" to HERO_PREFIX + "/api/explorer/rpc" : HERO_PREFIX + "/api/rpc".

Why: These three call sites are the only places in the UI layer that hit the /rpc and /explorer/rpc paths. All templates (index.html, nodes.html, vms.html, admin.html, explorer.html) go through rpc() / explorerRpc(), so updating these helpers migrates every consumer at once. sendRpcCall() is the OpenRPC playground's direct fetch and must be migrated separately.

Verification: grep the repo to confirm no remaining HERO_PREFIX + "/rpc" or HERO_PREFIX + "/explorer/rpc" literals exist in dashboard.js; only the new /api/rpc and /api/explorer/rpc paths should appear.

Step 3 — Smoke check (post-rebuild)

Files: none (runtime verification only).
Dependencies: Steps 1 and 2 complete; hero_compute_ui rebuilt and restarted.

  • Direct: POST http://127.0.0.1:9001/api/rpc with a valid JSON-RPC 2.0 body returns 200 + application/json.
  • Direct backward compatibility: POST http://127.0.0.1:9001/rpc still returns a valid response.
  • Through hero_router: load the compute dashboard inside Hero OS; "Register Node" must complete successfully with no Unexpected end of JSON input in the console.
  • Explorer: the "Try it" button on /explorer must execute method calls against both server and explorer OpenRPC specs.

Acceptance Criteria

  • server.rs exposes POST /api/rpc and OPTIONS /api/rpc wired to rpc_proxy / cors_preflight.
  • server.rs exposes POST /api/explorer/rpc and OPTIONS /api/explorer/rpc wired to explorer_rpc_proxy / cors_preflight.
  • server.rs still exposes legacy POST /rpc, POST /hero_compute/rpc, and POST /explorer/rpc routes unchanged.
  • dashboard.js rpc() helper targets HERO_PREFIX + "/api/rpc".
  • dashboard.js explorerRpc() helper targets HERO_PREFIX + "/api/explorer/rpc".
  • dashboard.js sendRpcCall() targets /api/rpc and /api/explorer/rpc.
  • Direct access at http://127.0.0.1:9001/api/rpc returns a valid JSON-RPC response.
  • Direct access at http://127.0.0.1:9001/rpc (legacy) still returns a valid JSON-RPC response.
  • Through hero_router / Hero OS, "Register Node" succeeds without Unexpected end of JSON input.
  • OpenRPC explorer "Try it" button works for both server and explorer specs.
  • cargo build -p hero_compute_ui succeeds; existing inline tests in server.rs still pass.

Notes

  • This is Option 1 only. Option 2 (adding a root /rpc route to hero_compute_server) will be filed as a separate follow-up.
  • No hero_compute_server, no hero_compute_sdk, no template changes.
  • Steps 1 and 2 touch independent files and can run in parallel. Step 3 runs after both land and the UI is rebuilt.
  • No database migration, no config changes, no new dependencies.
## Implementation Spec — Issue #93 (Option 1) ### Objective Add new UI proxy routes `/api/rpc` and `/api/explorer/rpc` alongside the existing `/rpc` and `/explorer/rpc` routes in `hero_compute_ui`, and update the dashboard JavaScript to call the new paths. This prevents `hero_router`'s sibling-shortcut logic (which re-dispatches any `POST /<svc>/ui/rpc*` to the service's `rpc.sock` at path `/rpc*`) from bypassing `ui.sock` and hitting a non-existent root `/rpc` endpoint on `hero_compute_server`. Because the router's shortcut matches the prefix `/rpc`, forwarding via `/api/rpc` sidesteps it entirely. This spec covers **Option 1 (UI-side workaround) only**. Option 2 (server-side fix adding a root `/rpc` route to `hero_compute_server`) is out of scope and will be addressed in a separate follow-up issue. ### Requirements - New routes `/api/rpc` and `/api/explorer/rpc` MUST be added to the UI Axum router and point to the existing `rpc_proxy` / `explorer_rpc_proxy` handlers with CORS preflight support, identical to the existing routes. - Old routes `/rpc`, `/hero_compute/rpc`, and `/explorer/rpc` MUST remain in place so direct `http://127.0.0.1:9001` access and external callers keep working unchanged. - The dashboard JavaScript helpers (`rpc()` and `explorerRpc()`) MUST call the new `/api/rpc` and `/api/explorer/rpc` paths so that every template consumer migrates in one change. - The hardcoded fetch URL inside `sendRpcCall()` (OpenRPC explorer page) MUST be updated to the new paths. - Through the router, `POST /hero_compute/ui/api/rpc` must reach: browser -> router -> `ui.sock /api/rpc` -> `rpc_proxy` -> compute server `/api/root/cloud/rpc`. Because the router's sibling-shortcut pattern only triggers on `ui/rpc*`, a request path starting with `ui/api/rpc` avoids the shortcut. - No changes to the RPC proxy handler internals — only new route bindings are added. - No server-side (`hero_compute_server`) changes; no `hero_compute_sdk` constant changes; no template changes. ### Files to modify - `crates/hero_compute_ui/src/server.rs` — add two new route bindings inside the `Router::new()` builder chain in `run_server()`, alongside the existing RPC proxy routes. Keep existing routes untouched. - `crates/hero_compute_ui/static/js/dashboard.js` — update three call sites: the `rpc()` helper (line ~127), the `explorerRpc()` helper (line ~128), and the hardcoded URL inside `sendRpcCall()` (line ~2170, OpenRPC explorer "Try it" button). No template (`*.html`) files need to change — every template uses the two helpers in `dashboard.js`. ### Implementation Plan #### Step 1 — Add new proxy routes in `server.rs` Files: `crates/hero_compute_ui/src/server.rs` Dependencies: none (parallel with Step 2). In the `Router::new()` chain inside `run_server()`, locate the comment `// RPC proxy routes (kept from original)` (currently around line 254). Immediately after the existing three route bindings: ```rust .route("/rpc", post(rpc_proxy).options(cors_preflight)) .route("/hero_compute/rpc", post(rpc_proxy).options(cors_preflight)) .route( "/explorer/rpc", post(explorer_rpc_proxy).options(cors_preflight), ) ``` add two additional bindings that reuse the same handlers: ```rust // New router-safe paths: bypass hero_router's /rpc* sibling-shortcut (issue #93) .route("/api/rpc", post(rpc_proxy).options(cors_preflight)) .route( "/api/explorer/rpc", post(explorer_rpc_proxy).options(cors_preflight), ) ``` Why: `hero_router`'s sibling-shortcut intercepts any `POST /<svc>/ui/rpc*` and re-dispatches it to the service's `rpc.sock` at path `/rpc*`. Since `hero_compute_server` does not expose a root `/rpc` (it serves under `/api/root/cloud/rpc`), the shortcut causes a 404 with empty body and the browser's `r.json()` throws `Unexpected end of JSON input`. A route whose path does not start with `/rpc` is not intercepted, so `/api/rpc` and `/api/explorer/rpc` reach the UI proxy intact. Verification: `cargo build -p hero_compute_ui` succeeds 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.js` Dependencies: none (parallel with Step 1; ship Steps 1 and 2 together). Three edits: 1. Line ~127, in the `rpc()` helper — change `HERO_PREFIX + "/rpc"` to `HERO_PREFIX + "/api/rpc"`. 2. Line ~128, in the `explorerRpc()` helper — change `HERO_PREFIX + "/explorer/rpc"` to `HERO_PREFIX + "/api/explorer/rpc"`. 3. Line ~2170, inside `sendRpcCall()` — change the ternary URL from `HERO_PREFIX + "/explorer/rpc" : HERO_PREFIX + "/rpc"` to `HERO_PREFIX + "/api/explorer/rpc" : HERO_PREFIX + "/api/rpc"`. Why: These three call sites are the only places in the UI layer that hit the `/rpc` and `/explorer/rpc` paths. All templates (`index.html`, `nodes.html`, `vms.html`, `admin.html`, `explorer.html`) go through `rpc()` / `explorerRpc()`, so updating these helpers migrates every consumer at once. `sendRpcCall()` is the OpenRPC playground's direct `fetch` and must be migrated separately. Verification: grep the repo to confirm no remaining `HERO_PREFIX + "/rpc"` or `HERO_PREFIX + "/explorer/rpc"` literals exist in `dashboard.js`; only the new `/api/rpc` and `/api/explorer/rpc` paths should appear. #### Step 3 — Smoke check (post-rebuild) Files: none (runtime verification only). Dependencies: Steps 1 and 2 complete; `hero_compute_ui` rebuilt and restarted. - Direct: `POST http://127.0.0.1:9001/api/rpc` with a valid JSON-RPC 2.0 body returns 200 + `application/json`. - Direct backward compatibility: `POST http://127.0.0.1:9001/rpc` still returns a valid response. - Through `hero_router`: load the compute dashboard inside Hero OS; "Register Node" must complete successfully with no `Unexpected end of JSON input` in the console. - Explorer: the "Try it" button on `/explorer` must execute method calls against both server and explorer OpenRPC specs. ### Acceptance Criteria - [ ] `server.rs` exposes `POST /api/rpc` and `OPTIONS /api/rpc` wired to `rpc_proxy` / `cors_preflight`. - [ ] `server.rs` exposes `POST /api/explorer/rpc` and `OPTIONS /api/explorer/rpc` wired to `explorer_rpc_proxy` / `cors_preflight`. - [ ] `server.rs` still exposes legacy `POST /rpc`, `POST /hero_compute/rpc`, and `POST /explorer/rpc` routes unchanged. - [ ] `dashboard.js` `rpc()` helper targets `HERO_PREFIX + "/api/rpc"`. - [ ] `dashboard.js` `explorerRpc()` helper targets `HERO_PREFIX + "/api/explorer/rpc"`. - [ ] `dashboard.js` `sendRpcCall()` targets `/api/rpc` and `/api/explorer/rpc`. - [ ] Direct access at `http://127.0.0.1:9001/api/rpc` returns a valid JSON-RPC response. - [ ] Direct access at `http://127.0.0.1:9001/rpc` (legacy) still returns a valid JSON-RPC response. - [ ] Through `hero_router` / Hero OS, "Register Node" succeeds without `Unexpected end of JSON input`. - [ ] OpenRPC explorer "Try it" button works for both `server` and `explorer` specs. - [ ] `cargo build -p hero_compute_ui` succeeds; existing inline tests in `server.rs` still pass. ### Notes - This is Option 1 only. Option 2 (adding a root `/rpc` route to `hero_compute_server`) will be filed as a separate follow-up. - No `hero_compute_server`, no `hero_compute_sdk`, no template changes. - Steps 1 and 2 touch independent files and can run in parallel. Step 3 runs after both land and the UI is rebuilt. - No database migration, no config changes, no new dependencies.
Author
Member

Test Results

Command: cargo test -p hero_compute_ui

  • Total: 8
  • Passed: 8
  • Failed: 0

Workspace build: cargo build PASS

All 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.

## Test Results **Command:** `cargo test -p hero_compute_ui` - Total: 8 - Passed: 8 - Failed: 0 **Workspace build:** `cargo build` PASS All 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.
Author
Member

Summary

Implemented Option 1 (UI-side workaround) for issue #93. When hero_compute_ui is embedded behind hero_router inside 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/rpc and /api/explorer/rpc proxy routes in hero_compute_ui that the router does not intercept, and updates the dashboard JavaScript to target them. The existing /rpc, /hero_compute/rpc, and /explorer/rpc routes 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 the Router::new() chain in run_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 calls HERO_PREFIX + "/api/rpc", explorerRpc() helper now calls HERO_PREFIX + "/api/explorer/rpc", and the sendRpcCall() ternary now selects between HERO_PREFIX + "/api/explorer/rpc" and HERO_PREFIX + "/api/rpc". Grep post-checks confirm no remaining HERO_PREFIX + "/rpc" or HERO_PREFIX + "/explorer/rpc" literals.

Test results

  • cargo test -p hero_compute_ui: 8 total, 8 passed, 0 failed (all sanitize_base_path variants, middleware header handling, and test_normalize_socket_path).
  • cargo build (workspace): PASS.
  • JavaScript changes are not covered by Rust tests and are verified via the smoke checks below.

Smoke checks pending

After deploy, the following runtime checks must be verified:

  1. Direct POST to 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").
  2. Legacy POST to http://127.0.0.1:9001/rpc — should still work when calling the UI directly (backward compatibility preserved).
  3. Through hero_router from 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_router to narrow the /rpc* sibling-shortcut) remains as a potential follow-up.

## Summary Implemented Option 1 (UI-side workaround) for issue #93. When `hero_compute_ui` is embedded behind `hero_router` inside 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/rpc` and `/api/explorer/rpc` proxy routes in `hero_compute_ui` that the router does not intercept, and updates the dashboard JavaScript to target them. The existing `/rpc`, `/hero_compute/rpc`, and `/explorer/rpc` routes 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 the `Router::new()` chain in `run_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 calls `HERO_PREFIX + "/api/rpc"`, `explorerRpc()` helper now calls `HERO_PREFIX + "/api/explorer/rpc"`, and the `sendRpcCall()` ternary now selects between `HERO_PREFIX + "/api/explorer/rpc"` and `HERO_PREFIX + "/api/rpc"`. Grep post-checks confirm no remaining `HERO_PREFIX + "/rpc"` or `HERO_PREFIX + "/explorer/rpc"` literals. ## Test results - `cargo test -p hero_compute_ui`: 8 total, 8 passed, 0 failed (all `sanitize_base_path` variants, middleware header handling, and `test_normalize_socket_path`). - `cargo build` (workspace): PASS. - JavaScript changes are not covered by Rust tests and are verified via the smoke checks below. ## Smoke checks pending After deploy, the following runtime checks must be verified: 1. Direct POST to `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"). 2. Legacy POST to `http://127.0.0.1:9001/rpc` — should still work when calling the UI directly (backward compatibility preserved). 3. Through `hero_router` from 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_router` to narrow the `/rpc*` sibling-shortcut) remains as a potential follow-up.
mahmoud added this to the ACTIVE project 2026-04-20 14:01:35 +00:00
mahmoud added this to the now milestone 2026-04-20 14:01:37 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_compute#93
No description provided.