Voice UI broken when served behind hero_router — multiple path resolution issues #10
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_voice#10
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 Voice UI (
hero_voice_ui) does not function when accessed through hero_router (e.g. at/hero_voice/ui/).This affects both standalone access via hero_router and embedding inside hero_os as an iframe.
Issues Found
1.
app.jsfails to load (404)The HTML loads
app.jswith a relative path. When the page is served at/hero_voice/ui(hero_router strips the trailing slash via 308 redirect), the browser resolvesapp.jsto/hero_voice/app.jsinstead of/hero_voice/ui/app.js.crates/hero_voice_ui/static/index.htmlline 9922. RPC calls go to wrong endpoint
app.jshardcodesconst RPC_URL = '/rpc'as an absolute path. When behind hero_router, this hits the router's own management RPC at/rpcinstead of/hero_voice/rpc.crates/hero_voice_ui/static/app.jsline 103. Health check polling returns 404
The connection status widget in
index.htmlcallsfetch('health')which resolves to/hero_voice/healthinstead of/hero_voice/ui/health.crates/hero_voice_ui/static/index.html(inline script at bottom)How to Reproduce
hero_proc,hero_osis,hero_router,hero_os,hero_voicehttp://127.0.0.1:9988/hero_os/ui/app.js,healthSame path resolution issue also affects the WebSocket endpoint.
app.js:1174hardcodesnew WebSocket("${protocol}//${window.location.host}/ws")which hits hero_router instead of/hero_voice/ui/ws.This causes recording to fail silently — the WebSocket errors out immediately, triggering
stopRecording()and anInvalidStateError: Cannot close a closed AudioContext.Implementation Spec for Issue #10
Objective
Make the hero_voice_ui frontend work correctly both when accessed directly and when served behind hero_router at any prefix path (e.g.,
/hero_voice/ui/). All URL references (script loading, RPC calls, health checks, WebSocket connections, file URLs) must resolve correctly regardless of the base path, using theX-Forwarded-Prefixheader that hero_router injects.Requirements
app.jsusing a path that works at any mount point./rpcendpoint relative to the UI's mount path, not the absolute root.healthrelative to the UI's actual served path./ws)./files/audio/...) must be prefixed correctly./rpcroute must be added to the UI's Axum router since it is currently missing entirely.X-Forwarded-Prefixor from the browser's current page URL.Files to Modify
crates/hero_voice_ui/src/main.rs/rpcproxy route. Enhance middleware to storeX-Forwarded-Prefixas BasePath extension. Modifyserve_staticto inject<meta name="base-path">intoindex.htmlwhen prefix is present.crates/hero_voice_ui/static/app.jsRPC_URLdynamically from base-path meta tag. Fix WebSocket URL to useBASEprefix. Fix/files/audio/...and/files/transforms/...URLs.crates/hero_voice_ui/static/index.html<script src="app.js">to<script src="./app.js">. Fix inline health-check script to use base-path-derived URLs.Implementation Plan
Step 1: Add
/rpcproxy route andBasePathextension to the Axum serverFiles:
crates/hero_voice_ui/src/main.rsBasePathnewtype structhero_context_middlewareto extractX-Forwarded-Prefixand insertBasePathextensionrpc_proxy_handlerfunction using existingproxy_to_socket.route("/rpc", post(rpc_proxy_handler))to the routerserve_staticto inject<meta name="base-path">tag intoindex.htmlwhen BasePath is presentDependencies: none
Step 2: Fix JavaScript to derive all URLs from the base path
Files:
crates/hero_voice_ui/static/app.jsBASEconstant that reads from<meta name="base-path">tag with fallbackRPC_URL = '/rpc'withRPC_URL = BASE + '/rpc'BASE + '/ws'BASE + '/files/audio/...'BASE + '/files/transforms/...'Dependencies: none (works independently, reads meta tag at runtime)
Step 3: Fix index.html script tag and inline health-check script
Files:
crates/hero_voice_ui/static/index.html<script src="app.js">to<script src="./app.js">BASEfrom meta tagfetch('health')withBASE + '/health'fetch('rpc', ...)withBASE + '/rpc'Dependencies: none (works independently, reads meta tag at runtime)
Acceptance Criteria
/rpcroute exists on the hero_voice_ui Axum server that proxies JSON-RPC requests to the backend socket/hero_voice/ui/,app.jsloads successfully (no 404){prefix}/rpcand reach the UI's proxy{prefix}/healthand returns 200{prefix}/ws<meta name="base-path">is injected only whenX-Forwarded-Prefixis present/hero-bootstrap-bridge.csslink remains absolute (served by hero_router at root)Notes
/rpcroute is a pre-existing bug --proxy_to_socketexists but was never mounted as an HTTP route.hero-bootstrap-bridge.csslink on line 19 uses an absolute path intentionally -- it is served by hero_router at the root level.Test Results
Build: pass
Tests: Total: 13, Passed: 13, Failed: 0
All 13 unit tests passed across 7 test binaries and 3 doc-test suites (3 doc-tests ignored). Build completed with warnings only (unused imports, dead code, deprecated methods, unused mut variables) -- no errors.
Test breakdown:
Branch: development
Commit:
8cb9ec5Implementation Summary
Changes Made
crates/hero_voice_ui/src/main.rs
BasePathnewtype struct to carry the URL prefix through request extensionshero_context_middlewareto extractX-Forwarded-Prefixheader, trim trailing slashes, and insertBasePathinto request extensionsrpc_handlerfunction that proxies JSON-RPC requests to the backend viaproxy_to_socket/rpcroute to the Axum router (this was completely missing)serve_staticto accept the full request, extractBasePath, and inject a<meta name="base-path">tag intoindex.htmlwhen a prefix is presentcrates/hero_voice_ui/static/app.js
BASEconstant that reads from<meta name="base-path">tag (injected by server when behind proxy), with fallback to deriving fromwindow.location.pathnameRPC_URLfrom hardcoded/rpctoBASE + '/rpc'BASE(/files/audio/...->BASE + '/files/audio/...')BASE(/files/transforms/...->BASE + '/files/transforms/...')/wstoBASE + '/ws'crates/hero_voice_ui/static/index.html
<script src="app.js">to<script src="./app.js">for explicit relative resolutionBbase-path variable to the inline connection status script (reads from<meta name="base-path">tag)fetch('health')tofetch(B+'/health')andfetch('rpc',...)tofetch(B+'/rpc',...)in the polling functionTest Results
Notes
/rpcroute was completely absent from the UI server. While it may have appeared to work previously because hero_router proxied/rpcdirectly to the backend server socket, this meant the UI could not proxy RPC requests independently. The new route uses the existingproxy_to_socketfunction.<meta name="base-path">tag is only injected whenX-Forwarded-Prefixis present. When accessed directly (no reverse proxy), no tag is injected andBASEevaluates to empty string, preserving existing behavior./hero-bootstrap-bridge.csslink remains absolute as intended (served by hero_router at root).Pull request opened: #14
This PR implements the changes discussed in this issue.