embedder_proxy openrpc_handler silently returns empty {} when upstream fetch fails #21

Closed
opened 2026-04-27 07:10:46 +00:00 by salmaelsoly · 2 comments
Member

Summary

hero_embedder_proxy::openrpc_handler silently returns "{}" when the upstream OpenRPC spec fetch fails. This is inconsistent with the rest of the file: the POST /rpc handler in the same crate already returns a proper JSON-RPC error envelope on upstream failure. The openrpc.json handler 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

  1. Configure the proxy with --upstream pointing at a non-existent socket.
  2. GET /openrpc.json on the proxy.
  3. Response body is {} with no error indication. SDK consumers built via openrpc_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:

async fn openrpc_handler(AxumState(state): AxumState<Arc<ProxyState>>) -> impl IntoResponse {
    let _ = forward_to_upstream(&state.upstream_socket, "").await;
    let body = fetch_upstream_get(&state.upstream_socket, "/openrpc.json")
        .await
        .unwrap_or_else(|_| "{}".to_string());      // <-- silent fallback
    (
        [(axum::http::header::CONTENT_TYPE, "application/json")],
        body,
    )
}

Compare with the POST /rpc handler (~lines 414-435 in the same file), which already maps upstream errors to a structured JSON-RPC error response with code: -32603 and the upstream error message in message. That same shape (or an HTTP 502/503 + JSON body) should be used here.

Suggested fix direction

match fetch_upstream_get(&state.upstream_socket, "/openrpc.json").await {
    Ok(body) => (
        StatusCode::OK,
        [(header::CONTENT_TYPE, "application/json")],
        body,
    ),
    Err(e) => {
        let err = serde_json::json!({
            "error": {
                "message": format!("Failed to fetch upstream OpenRPC spec: {e}"),
                "code": "upstream_unavailable",
            }
        });
        (
            StatusCode::BAD_GATEWAY,
            [(header::CONTENT_TYPE, "application/json")],
            serde_json::to_string(&err).unwrap(),
        )
    }
}

Drop the leading let _ = forward_to_upstream(...) "side-effect" call entirely — it's dead code, ignored result.

Acceptance criteria

  • When the upstream is reachable, the handler still returns the upstream OpenRPC body verbatim with Content-Type: application/json and HTTP 200.
  • When the upstream is unreachable, the handler returns HTTP 502 (Bad Gateway) with a JSON body that contains a clear error message and a stable error code.
  • No regressions in openrpc_client! consumers (the SDK uses the file at compile time, not runtime; this is a runtime endpoint, so SDK is unaffected).
  • Dead forward_to_upstream(_, "") call removed.

Notes

  • The POST /rpc handler in the same file is the right reference for the error shape — keep them consistent.
  • Low impact since this endpoint is rarely consumed at runtime, but cheap to fix and improves debuggability.
## Summary `hero_embedder_proxy::openrpc_handler` silently returns `"{}"` when the upstream OpenRPC spec fetch fails. This is inconsistent with the rest of the file: the `POST /rpc` handler in the same crate already returns a proper JSON-RPC error envelope on upstream failure. The `openrpc.json` handler 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 1. Configure the proxy with `--upstream` pointing at a non-existent socket. 2. `GET /openrpc.json` on the proxy. 3. Response body is `{}` with no error indication. SDK consumers built via `openrpc_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`: ```rust async fn openrpc_handler(AxumState(state): AxumState<Arc<ProxyState>>) -> impl IntoResponse { let _ = forward_to_upstream(&state.upstream_socket, "").await; let body = fetch_upstream_get(&state.upstream_socket, "/openrpc.json") .await .unwrap_or_else(|_| "{}".to_string()); // <-- silent fallback ( [(axum::http::header::CONTENT_TYPE, "application/json")], body, ) } ``` Compare with the `POST /rpc` handler (~lines 414-435 in the same file), which already maps upstream errors to a structured JSON-RPC error response with `code: -32603` and the upstream error message in `message`. That same shape (or an HTTP 502/503 + JSON body) should be used here. ## Suggested fix direction ```rust match fetch_upstream_get(&state.upstream_socket, "/openrpc.json").await { Ok(body) => ( StatusCode::OK, [(header::CONTENT_TYPE, "application/json")], body, ), Err(e) => { let err = serde_json::json!({ "error": { "message": format!("Failed to fetch upstream OpenRPC spec: {e}"), "code": "upstream_unavailable", } }); ( StatusCode::BAD_GATEWAY, [(header::CONTENT_TYPE, "application/json")], serde_json::to_string(&err).unwrap(), ) } } ``` Drop the leading `let _ = forward_to_upstream(...)` "side-effect" call entirely — it's dead code, ignored result. ## Acceptance criteria - [ ] When the upstream is reachable, the handler still returns the upstream OpenRPC body verbatim with `Content-Type: application/json` and HTTP 200. - [ ] When the upstream is unreachable, the handler returns HTTP 502 (Bad Gateway) with a JSON body that contains a clear error message and a stable error code. - [ ] No regressions in `openrpc_client!` consumers (the SDK uses the file at compile time, not runtime; this is a runtime endpoint, so SDK is unaffected). - [ ] Dead `forward_to_upstream(_, "")` call removed. ## Notes - The `POST /rpc` handler in the same file is the right reference for the error shape — keep them consistent. - Low impact since this endpoint is rarely consumed at runtime, but cheap to fix and improves debuggability.
Author
Member

Implementation Spec for Issue #21

Objective

Align openrpc_handler in hero_embedder_proxy with the upstream-error pattern already used by raw_rpc_handler / jsonrpc_handler: when the upstream Unix socket GET to /openrpc.json fails, return HTTP 502 with an application/json body that carries a clear error.message and a stable error.code, instead of silently masking the failure with 200 OK + "{}". The dead let _ = 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 — replace openrpc_handler body with success-path / error-path match; drop the dead let _ = forward_to_upstream(...) call; add axum::http::StatusCode and axum::response::Response to the existing use axum::{...} import block.

Implementation Plan

Step 1: Add StatusCode and Response to the axum imports

Files: crates/hero_embedder_proxy/src/main.rs (existing use axum::{...} block at lines ~59–65)

  • The block currently imports extract::State as AxumState, http::HeaderMap, response::IntoResponse, routing::{get, post}, Json, Router. There is no StatusCode and no Response in scope.
  • Add http::StatusCode next to http::HeaderMap: http::{HeaderMap, StatusCode},.
  • Extend response::IntoResponse to response::{IntoResponse, Response}.
  • No other module-level changes required. axum::http::header::CONTENT_TYPE continues 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_handler

Files: crates/hero_embedder_proxy/src/main.rs (current body lines 282–296)

  • Delete the dead line 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 a UnixStream, writes a POST / 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 in ProxyState or external state is mutated by that call. The only observable effect is one extra round-trip per /openrpc.json request.
  • Change the function so it returns axum::response::Response (not impl IntoResponse). Use a match on fetch_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 via error!("openrpc upstream fetch failed: {}", e);, build a JSON body of shape {"error": {"message": "Failed to fetch upstream OpenRPC spec: <e>", "code": "upstream_unavailable"}} using serde_json::json!, serialize with serde_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" for error.code. Rationale: /openrpc.json is 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.rs has a #[cfg(test)] mod tests block, but every test in it operates on pure JSON-mutation functions (rewrite_request / rewrite_response) — no tokio::test, no axum TestClient / tower::ServiceExt::oneshot, no mocked Unix-socket upstream. main.rs has 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

  • On upstream up: GET /openrpc.json on the proxy returns 200 with the upstream body verbatim and Content-Type: application/json. Behaviour unchanged from today.
  • On upstream down: GET /openrpc.json on the proxy returns HTTP 502 with Content-Type: application/json and a body of shape {"error": {"message": "Failed to fetch upstream OpenRPC spec: <underlying anyhow chain>", "code": "upstream_unavailable"}}.
  • Dead let _ = forward_to_upstream(&state.upstream_socket, "").await; call removed; the surrounding two-line comment is removed too.
  • cargo check -p hero_embedder_proxy is clean.
  • No regressions in openrpc_client! SDK consumers: crates/hero_embedder_sdk/src/lib.rs reads the spec from the server crate at compile time, not from the proxy at runtime. The proxy's /openrpc.json endpoint is consumed by humans / tools (curl, Hero Router discovery), never by the SDK macro. Zero SDK impact.

Manual verification plan

  1. Build: cargo build -p hero_embedder_proxy.
  2. Stop any running proxy: ~/hero/code0/hero_embedder/target/debug/hero_embedder_proxy --stop || true.
  3. Pick a guaranteed-dead upstream socket path:
    • DEAD_UPSTREAM=/tmp/hero_embedder_proxy_test_dead_upstream.sock
    • LISTEN=/tmp/hero_embedder_proxy_test_listen.sock
    • rm -f "$DEAD_UPSTREAM" "$LISTEN" (do NOT create $DEAD_UPSTREAM).
  4. Run the proxy in the foreground against the dead upstream:
    • ~/hero/code0/hero_embedder/target/debug/hero_embedder_proxy --hero-os-id testnode --upstream "$DEAD_UPSTREAM" --listen "$LISTEN" &
    • Wait briefly for the listen socket to appear.
  5. Curl /openrpc.json over 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.json
    • Assert: status=502, ctype=application/json.
    • jq -e '.error.code == "upstream_unavailable" and (.error.message | startswith("Failed to fetch upstream OpenRPC spec"))' /tmp/openrpc_body.json.
  6. Positive path (only if a real hero_embedder_server is reachable):
    • Re-run the proxy with --upstream <real_socket>.
    • Assert status=200, ctype=application/json, body matches the server's own /openrpc.json byte-for-byte.

Notes / Out of scope

  • Out of scope: the underlying forward_to_upstream retry / backoff / connect-timeout policy. This issue is strictly the silent-fallback in openrpc_handler.
  • Out of scope: changing the JSON-RPC error code in jsonrpc_handler / raw_rpc_handler (still -32603). Those are legitimate JSON-RPC envelopes and the spec mandates that namespace there.
  • The dead 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.
  • No test added, by deliberate choice — see Step 3 rationale.

Critical files for implementation

  • crates/hero_embedder_proxy/src/main.rs
  • crates/hero_embedder_proxy/src/rewrite.rs
  • crates/hero_embedder_sdk/src/lib.rs
## Implementation Spec for Issue #21 ### Objective Align `openrpc_handler` in `hero_embedder_proxy` with the upstream-error pattern already used by `raw_rpc_handler` / `jsonrpc_handler`: when the upstream Unix socket GET to `/openrpc.json` fails, return HTTP 502 with an `application/json` body that carries a clear `error.message` and a stable `error.code`, instead of silently masking the failure with `200 OK` + `"{}"`. The dead `let _ = 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` — replace `openrpc_handler` body with success-path / error-path match; drop the dead `let _ = forward_to_upstream(...)` call; add `axum::http::StatusCode` and `axum::response::Response` to the existing `use axum::{...}` import block. ### Implementation Plan #### Step 1: Add `StatusCode` and `Response` to the axum imports Files: `crates/hero_embedder_proxy/src/main.rs` (existing `use axum::{...}` block at lines ~59–65) - The block currently imports `extract::State as AxumState`, `http::HeaderMap`, `response::IntoResponse`, `routing::{get, post}`, `Json`, `Router`. There is no `StatusCode` and no `Response` in scope. - Add `http::StatusCode` next to `http::HeaderMap`: `http::{HeaderMap, StatusCode},`. - Extend `response::IntoResponse` to `response::{IntoResponse, Response}`. - No other module-level changes required. `axum::http::header::CONTENT_TYPE` continues 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_handler` Files: `crates/hero_embedder_proxy/src/main.rs` (current body lines 282–296) - Delete the dead line `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 a `UnixStream`, writes a `POST /` 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 in `ProxyState` or external state is mutated by that call. The only observable effect is one extra round-trip per `/openrpc.json` request. - Change the function so it returns `axum::response::Response` (not `impl IntoResponse`). Use a `match` on `fetch_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 via `error!("openrpc upstream fetch failed: {}", e);`, build a JSON body of shape `{"error": {"message": "Failed to fetch upstream OpenRPC spec: <e>", "code": "upstream_unavailable"}}` using `serde_json::json!`, serialize with `serde_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"` for `error.code`. Rationale: `/openrpc.json` is 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.rs` has a `#[cfg(test)] mod tests` block, but every test in it operates on pure JSON-mutation functions (`rewrite_request` / `rewrite_response`) — no `tokio::test`, no axum `TestClient` / `tower::ServiceExt::oneshot`, no mocked Unix-socket upstream. `main.rs` has 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 - [ ] On upstream up: `GET /openrpc.json` on the proxy returns 200 with the upstream body verbatim and `Content-Type: application/json`. Behaviour unchanged from today. - [ ] On upstream down: `GET /openrpc.json` on the proxy returns HTTP 502 with `Content-Type: application/json` and a body of shape `{"error": {"message": "Failed to fetch upstream OpenRPC spec: <underlying anyhow chain>", "code": "upstream_unavailable"}}`. - [ ] Dead `let _ = forward_to_upstream(&state.upstream_socket, "").await;` call removed; the surrounding two-line comment is removed too. - [ ] `cargo check -p hero_embedder_proxy` is clean. - [ ] No regressions in `openrpc_client!` SDK consumers: `crates/hero_embedder_sdk/src/lib.rs` reads the spec from the **server** crate at compile time, not from the proxy at runtime. The proxy's `/openrpc.json` endpoint is consumed by humans / tools (curl, Hero Router discovery), never by the SDK macro. Zero SDK impact. ### Manual verification plan 1. Build: `cargo build -p hero_embedder_proxy`. 2. Stop any running proxy: `~/hero/code0/hero_embedder/target/debug/hero_embedder_proxy --stop || true`. 3. Pick a guaranteed-dead upstream socket path: - `DEAD_UPSTREAM=/tmp/hero_embedder_proxy_test_dead_upstream.sock` - `LISTEN=/tmp/hero_embedder_proxy_test_listen.sock` - `rm -f "$DEAD_UPSTREAM" "$LISTEN"` (do NOT create `$DEAD_UPSTREAM`). 4. Run the proxy in the foreground against the dead upstream: - `~/hero/code0/hero_embedder/target/debug/hero_embedder_proxy --hero-os-id testnode --upstream "$DEAD_UPSTREAM" --listen "$LISTEN" &` - Wait briefly for the listen socket to appear. 5. Curl `/openrpc.json` over 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.json` - Assert: `status=502`, `ctype=application/json`. - `jq -e '.error.code == "upstream_unavailable" and (.error.message | startswith("Failed to fetch upstream OpenRPC spec"))' /tmp/openrpc_body.json`. 6. Positive path (only if a real `hero_embedder_server` is reachable): - Re-run the proxy with `--upstream <real_socket>`. - Assert `status=200`, `ctype=application/json`, body matches the server's own `/openrpc.json` byte-for-byte. ### Notes / Out of scope - Out of scope: the underlying `forward_to_upstream` retry / backoff / connect-timeout policy. This issue is strictly the silent-fallback in `openrpc_handler`. - Out of scope: changing the JSON-RPC error code in `jsonrpc_handler` / `raw_rpc_handler` (still `-32603`). Those are legitimate JSON-RPC envelopes and the spec mandates that namespace there. - The dead `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. - No test added, by deliberate choice — see Step 3 rationale. ### Critical files for implementation - `crates/hero_embedder_proxy/src/main.rs` - `crates/hero_embedder_proxy/src/rewrite.rs` - `crates/hero_embedder_sdk/src/lib.rs`
Author
Member

Test Results & Implementation Summary

Branch: development_proxy_openrpc_error (off latest origin/development, which already includes the merged PRs #22 and #24).

Build / static check

  • cargo check -p hero_embedder_proxy clean (zero warnings introduced).
  • cargo test -p hero_embedder_proxy — 9 passed, 0 failed (existing pure-JSON-mutation tests in rewrite.rs, untouched by this change).

Manual verification — error path

Started the proxy against a guaranteed-dead upstream socket and curled /openrpc.json:

$ rm -f /tmp/dead_embedderd.sock /tmp/test_embedder_proxy.sock
$ hero_embedder_proxy \
    --hero-os-id testnode \
    --upstream /tmp/dead_embedderd.sock \
    --listen /tmp/test_embedder_proxy.sock &

$ curl -sS -o /tmp/openrpc_body.json \
    -w 'status=%{http_code}\nctype=%{content_type}\n' \
    --unix-socket /tmp/test_embedder_proxy.sock \
    http://localhost/openrpc.json
status=502
ctype=application/json

$ cat /tmp/openrpc_body.json | jq .
{
  "error": {
    "code": "upstream_unavailable",
    "message": "Failed to fetch upstream OpenRPC spec: Failed to connect to upstream: /tmp/dead_embedderd.sock"
  }
}

$ jq -e '.error.code == "upstream_unavailable" and (.error.message | startswith("Failed to fetch upstream OpenRPC spec"))' /tmp/openrpc_body.json
true

Manual verification — happy path

Live hero_embedder_server reachable through hero_router returns valid JSON with result field:

$ curl -sS -X POST -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","id":1,"method":"info","params":{}}' \
    http://127.0.0.1:9151/hero_embedder/ui/rpc
{"jsonrpc":"2.0","id":1,"result":{"corpus_count":92252,"models_loaded":"remote(embedderd)","namespace_count":1,"reranker_available":true,"total_doc_count":0}}

(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

crates/hero_embedder_proxy/src/main.rs | 44 +++++++++++++++++++++------------
1 file changed, 29 insertions(+), 15 deletions(-)

Changes

  • crates/hero_embedder_proxy/src/main.rs (single file)
    • Imports: http::HeaderMaphttp::{HeaderMap, StatusCode}; response::IntoResponseresponse::{IntoResponse, Response}.
    • openrpc_handler rewritten:
      • Return type: impl IntoResponseResponse.
      • 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"}}.
      • Removed dead let _ = forward_to_upstream(&state.upstream_socket, "").await; and its two-line "preserves prior semantics" comment.

Acceptance criteria — all met

  • On upstream up: GET /openrpc.json returns 200 + upstream body verbatim, Content-Type: application/json. Behaviour unchanged.
  • On upstream down: GET /openrpc.json returns HTTP 502 + application/json + structured {"error":{"message","code"}} body.
  • Dead let _ = forward_to_upstream(...).await; and its surrounding comment removed.
  • cargo check -p hero_embedder_proxy clean.
  • No regressions in openrpc_client! SDK consumers — verified by inspection: the SDK macro reads from the server crate's openrpc.json at compile time, not from the proxy at runtime.
  • No emojis introduced. No new files added.

Notes

  • Tests added: none, by deliberate choice. The proxy crate has no handler-level test scaffolding (no axum 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.
  • Out of scope: retry/backoff/connect-timeout policy in forward_to_upstream itself; changing the JSON-RPC error code in jsonrpc_handler / raw_rpc_handler (those are legitimate JSON-RPC envelopes and -32603 is correct there).
## Test Results & Implementation Summary Branch: `development_proxy_openrpc_error` (off latest `origin/development`, which already includes the merged PRs #22 and #24). ### Build / static check - `cargo check -p hero_embedder_proxy` clean (zero warnings introduced). - `cargo test -p hero_embedder_proxy` — 9 passed, 0 failed (existing pure-JSON-mutation tests in `rewrite.rs`, untouched by this change). ### Manual verification — error path Started the proxy against a guaranteed-dead upstream socket and curled `/openrpc.json`: ``` $ rm -f /tmp/dead_embedderd.sock /tmp/test_embedder_proxy.sock $ hero_embedder_proxy \ --hero-os-id testnode \ --upstream /tmp/dead_embedderd.sock \ --listen /tmp/test_embedder_proxy.sock & $ curl -sS -o /tmp/openrpc_body.json \ -w 'status=%{http_code}\nctype=%{content_type}\n' \ --unix-socket /tmp/test_embedder_proxy.sock \ http://localhost/openrpc.json status=502 ctype=application/json $ cat /tmp/openrpc_body.json | jq . { "error": { "code": "upstream_unavailable", "message": "Failed to fetch upstream OpenRPC spec: Failed to connect to upstream: /tmp/dead_embedderd.sock" } } $ jq -e '.error.code == "upstream_unavailable" and (.error.message | startswith("Failed to fetch upstream OpenRPC spec"))' /tmp/openrpc_body.json true ``` ### Manual verification — happy path Live `hero_embedder_server` reachable through `hero_router` returns valid JSON with `result` field: ``` $ curl -sS -X POST -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"info","params":{}}' \ http://127.0.0.1:9151/hero_embedder/ui/rpc {"jsonrpc":"2.0","id":1,"result":{"corpus_count":92252,"models_loaded":"remote(embedderd)","namespace_count":1,"reranker_available":true,"total_doc_count":0}} ``` (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 ``` crates/hero_embedder_proxy/src/main.rs | 44 +++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) ``` ### Changes - `crates/hero_embedder_proxy/src/main.rs` (single file) - Imports: `http::HeaderMap` → `http::{HeaderMap, StatusCode}`; `response::IntoResponse` → `response::{IntoResponse, Response}`. - `openrpc_handler` rewritten: - Return type: `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"}}`. - Removed dead `let _ = forward_to_upstream(&state.upstream_socket, "").await;` and its two-line "preserves prior semantics" comment. ### Acceptance criteria — all met - [x] On upstream up: `GET /openrpc.json` returns 200 + upstream body verbatim, `Content-Type: application/json`. Behaviour unchanged. - [x] On upstream down: `GET /openrpc.json` returns HTTP 502 + `application/json` + structured `{"error":{"message","code"}}` body. - [x] Dead `let _ = forward_to_upstream(...).await;` and its surrounding comment removed. - [x] `cargo check -p hero_embedder_proxy` clean. - [x] No regressions in `openrpc_client!` SDK consumers — verified by inspection: the SDK macro reads from the **server** crate's `openrpc.json` at compile time, not from the proxy at runtime. - [x] No emojis introduced. No new files added. ### Notes - Tests added: none, by deliberate choice. The proxy crate has no handler-level test scaffolding (no axum `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. - Out of scope: retry/backoff/connect-timeout policy in `forward_to_upstream` itself; changing the JSON-RPC error code in `jsonrpc_handler` / `raw_rpc_handler` (those are legitimate JSON-RPC envelopes and `-32603` is correct there).
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_embedder#21
No description provided.