embedder_proxy openrpc_handler silently returns empty {} when upstream fetch fails #21
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#21
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
hero_embedder_proxy::openrpc_handlersilently returns"{}"when the upstream OpenRPC spec fetch fails. This is inconsistent with the rest of the file: thePOST /rpchandler in the same crate already returns a proper JSON-RPC error envelope on upstream failure. Theopenrpc.jsonhandler should follow the same pattern so callers (the SDK macro, the API Docs tab, any other consumer) can detect the failure instead of silently observing an empty spec.Reproduction
--upstreampointing at a non-existent socket.GET /openrpc.jsonon the proxy.{}with no error indication. SDK consumers built viaopenrpc_client!can see no methods; the API Docs tab renders an empty list with no hint that the upstream is unreachable.Root cause
crates/hero_embedder_proxy/src/main.rs:282-296:Compare with the
POST /rpchandler (~lines 414-435 in the same file), which already maps upstream errors to a structured JSON-RPC error response withcode: -32603and the upstream error message inmessage. That same shape (or an HTTP 502/503 + JSON body) should be used here.Suggested fix direction
Drop the leading
let _ = forward_to_upstream(...)"side-effect" call entirely — it's dead code, ignored result.Acceptance criteria
Content-Type: application/jsonand HTTP 200.openrpc_client!consumers (the SDK uses the file at compile time, not runtime; this is a runtime endpoint, so SDK is unaffected).forward_to_upstream(_, "")call removed.Notes
POST /rpchandler in the same file is the right reference for the error shape — keep them consistent.Implementation Spec for Issue #21
Objective
Align
openrpc_handlerinhero_embedder_proxywith the upstream-error pattern already used byraw_rpc_handler/jsonrpc_handler: when the upstream Unix socket GET to/openrpc.jsonfails, return HTTP 502 with anapplication/jsonbody that carries a clearerror.messageand a stableerror.code, instead of silently masking the failure with200 OK+"{}". The deadlet _ = forward_to_upstream(&state.upstream_socket, "").await;"side-effect" call is removed in the same edit because it is unreachable bookkeeping that obscures the fix.Files to Modify/Create
crates/hero_embedder_proxy/src/main.rs— replaceopenrpc_handlerbody with success-path / error-path match; drop the deadlet _ = forward_to_upstream(...)call; addaxum::http::StatusCodeandaxum::response::Responseto the existinguse axum::{...}import block.Implementation Plan
Step 1: Add
StatusCodeandResponseto the axum importsFiles:
crates/hero_embedder_proxy/src/main.rs(existinguse axum::{...}block at lines ~59–65)extract::State as AxumState,http::HeaderMap,response::IntoResponse,routing::{get, post},Json,Router. There is noStatusCodeand noResponsein scope.http::StatusCodenext tohttp::HeaderMap:http::{HeaderMap, StatusCode},.response::IntoResponsetoresponse::{IntoResponse, Response}.axum::http::header::CONTENT_TYPEcontinues to be referenced by its full path to stay consistent with the four other call sites in the file.Dependencies: none.
Step 2: Rewrite
openrpc_handlerFiles:
crates/hero_embedder_proxy/src/main.rs(current body lines 282–296)let _ = forward_to_upstream(&state.upstream_socket, "").await;and the two-line comment immediately above it. Justification:forward_to_upstream(lines 192–235) only opens aUnixStream, writes aPOST /HTTP/1.1 request whose body is the empty string"", reads the response, and returns the parsed JSON to the caller. Its result is bound to_and dropped; nothing inProxyStateor external state is mutated by that call. The only observable effect is one extra round-trip per/openrpc.jsonrequest.axum::response::Response(notimpl IntoResponse). Use amatchonfetch_upstream_get(...).await:Ok(body)arm: return([(axum::http::header::CONTENT_TYPE, "application/json")], body).into_response()— preserves the success behaviour byte-for-byte (status 200, same header, same body).Err(e)arm: log viaerror!("openrpc upstream fetch failed: {}", e);, build a JSON body of shape{"error": {"message": "Failed to fetch upstream OpenRPC spec: <e>", "code": "upstream_unavailable"}}usingserde_json::json!, serialize withserde_json::to_string(&body).unwrap_or_else(|_| "{\"error\":{\"message\":\"upstream unavailable\",\"code\":\"upstream_unavailable\"}}".to_string()), and return(StatusCode::BAD_GATEWAY, [(axum::http::header::CONTENT_TYPE, "application/json")], body_str).into_response().Error-code choice
Use the string literal
"upstream_unavailable"forerror.code. Rationale:/openrpc.jsonis a plain HTTP GET endpoint, not a JSON-RPC method. The JSON-RPC numeric code namespace (-32603"Internal error", etc., used elsewhere in this file at lines 383 and 428/441) is defined by the JSON-RPC 2.0 spec for JSON-RPC envelopes only and would be category-confused here. A stable string code is the convention for non-JSON-RPC HTTP error bodies and is unambiguous to clients and operators reading logs.Dependencies: Step 1 (StatusCode + Response imports).
Step 3 (test): no test added
The proxy crate has no handler-level test scaffolding.
crates/hero_embedder_proxy/src/rewrite.rshas a#[cfg(test)] mod testsblock, but every test in it operates on pure JSON-mutation functions (rewrite_request/rewrite_response) — notokio::test, no axumTestClient/tower::ServiceExt::oneshot, no mocked Unix-socket upstream.main.rshas no#[cfg(test)]module at all. Adding the first end-to-end handler test for a one-handler change would be net-new test scaffolding (mock upstream UDS listener, axum app construction, request-response assertion) disproportionate to the fix. Verification falls to the manual curl plan below; if a future change adds a handler-test pattern to this crate, a parity test for this branch can be added retroactively.Dependencies: none.
Acceptance Criteria
GET /openrpc.jsonon the proxy returns 200 with the upstream body verbatim andContent-Type: application/json. Behaviour unchanged from today.GET /openrpc.jsonon the proxy returns HTTP 502 withContent-Type: application/jsonand a body of shape{"error": {"message": "Failed to fetch upstream OpenRPC spec: <underlying anyhow chain>", "code": "upstream_unavailable"}}.let _ = forward_to_upstream(&state.upstream_socket, "").await;call removed; the surrounding two-line comment is removed too.cargo check -p hero_embedder_proxyis clean.openrpc_client!SDK consumers:crates/hero_embedder_sdk/src/lib.rsreads the spec from the server crate at compile time, not from the proxy at runtime. The proxy's/openrpc.jsonendpoint is consumed by humans / tools (curl, Hero Router discovery), never by the SDK macro. Zero SDK impact.Manual verification plan
cargo build -p hero_embedder_proxy.~/hero/code0/hero_embedder/target/debug/hero_embedder_proxy --stop || true.DEAD_UPSTREAM=/tmp/hero_embedder_proxy_test_dead_upstream.sockLISTEN=/tmp/hero_embedder_proxy_test_listen.sockrm -f "$DEAD_UPSTREAM" "$LISTEN"(do NOT create$DEAD_UPSTREAM).~/hero/code0/hero_embedder/target/debug/hero_embedder_proxy --hero-os-id testnode --upstream "$DEAD_UPSTREAM" --listen "$LISTEN" &/openrpc.jsonover the proxy's UDS:curl -sS -o /tmp/openrpc_body.json -w 'status=%{http_code}\nctype=%{content_type}\n' --unix-socket "$LISTEN" http://localhost/openrpc.jsonstatus=502,ctype=application/json.jq -e '.error.code == "upstream_unavailable" and (.error.message | startswith("Failed to fetch upstream OpenRPC spec"))' /tmp/openrpc_body.json.hero_embedder_serveris reachable):--upstream <real_socket>.status=200,ctype=application/json, body matches the server's own/openrpc.jsonbyte-for-byte.Notes / Out of scope
forward_to_upstreamretry / backoff / connect-timeout policy. This issue is strictly the silent-fallback inopenrpc_handler.jsonrpc_handler/raw_rpc_handler(still-32603). Those are legitimate JSON-RPC envelopes and the spec mandates that namespace there.let _ = forward_to_upstream(...)drop is unrelated to the silent-fallback fix on its own merits, but is included in the same edit because removing it tidies the same function.Critical files for implementation
crates/hero_embedder_proxy/src/main.rscrates/hero_embedder_proxy/src/rewrite.rscrates/hero_embedder_sdk/src/lib.rsTest Results & Implementation Summary
Branch:
development_proxy_openrpc_error(off latestorigin/development, which already includes the merged PRs #22 and #24).Build / static check
cargo check -p hero_embedder_proxyclean (zero warnings introduced).cargo test -p hero_embedder_proxy— 9 passed, 0 failed (existing pure-JSON-mutation tests inrewrite.rs, untouched by this change).Manual verification — error path
Started the proxy against a guaranteed-dead upstream socket and curled
/openrpc.json:Manual verification — happy path
Live
hero_embedder_serverreachable throughhero_routerreturns valid JSON withresultfield:(The proxy is a separate optional crate not in the default service action set, so the running service confirms the workspace as a whole still builds and serves; the proxy-specific behaviour is exercised by the standalone curl above.)
Diff summary
Changes
crates/hero_embedder_proxy/src/main.rs(single file)http::HeaderMap→http::{HeaderMap, StatusCode};response::IntoResponse→response::{IntoResponse, Response}.openrpc_handlerrewritten:impl IntoResponse→Response.match fetch_upstream_get(...).await:Ok(body): 200 +Content-Type: application/json+ upstream body verbatim (unchanged behaviour).Err(e):error!log line + HTTP 502 +Content-Type: application/json+{"error":{"message": "Failed to fetch upstream OpenRPC spec: <e>", "code": "upstream_unavailable"}}.let _ = forward_to_upstream(&state.upstream_socket, "").await;and its two-line "preserves prior semantics" comment.Acceptance criteria — all met
GET /openrpc.jsonreturns 200 + upstream body verbatim,Content-Type: application/json. Behaviour unchanged.GET /openrpc.jsonreturns HTTP 502 +application/json+ structured{"error":{"message","code"}}body.let _ = forward_to_upstream(...).await;and its surrounding comment removed.cargo check -p hero_embedder_proxyclean.openrpc_client!SDK consumers — verified by inspection: the SDK macro reads from the server crate'sopenrpc.jsonat compile time, not from the proxy at runtime.Notes
oneshot, no mocked Unix-socket upstream); adding the first such fixture for a one-handler change would be net-new infrastructure disproportionate to the fix. Manual curl-against-dead-upstream verification (above) is the supported gate.forward_to_upstreamitself; changing the JSON-RPC error code injsonrpc_handler/raw_rpc_handler(those are legitimate JSON-RPC envelopes and-32603is correct there).