bug: OpenRPC UDS transport drops X-Hero-Context and X-Hero-Claims headers #42

Open
opened 2026-05-06 12:48:35 +00:00 by casper-stevens · 3 comments
Member

Summary

While investigating hero_biz issue #37, a fundamental issue was discovered in hero_rpc's OsisClient and OpenRpcTransport that breaks context selection across the Hero OS ecosystem.

Root Causes

1. URL Path Construction (Layer 1)

OsisClient::new (Native) and endpoint (WASM) incorrectly construct the RPC endpoint using legacy per-domain or context-prefixed paths (e.g., {base_url}/hero_osis_{domain}/rpc).

Requirement: Per hero_context and the Unified OSIS Server model, the endpoint must be a simple {base_url}/rpc. Routing is handled via method prefixes and the X-Hero-Context header.

2. Header Forwarding on UDS (Layer 2)

The OpenRpcTransport implementation for Unix Domain Sockets (UDS) was found to drop HTTP headers. A comment in transport.rs stated: "Unix-socket transport currently drops the headers — raw socket framing has no place to carry them".

Correction: Since we use HTTP-over-UDS, we can and must carry headers like X-Hero-Context and X-Hero-Claims in the HTTP request framing over the socket. These headers are mandatory for context isolation and claim-based authorization.

Impact

All services using hero_rpc for context-aware OSIS calls are currently failing to transmit the context selector to the server, resulting in "silent" failures where all contexts return the same (default) data.

Proposed Fix

  1. Transport: Update http_post_unix and post_raw_json_with_headers in crates/openrpc/src/transport.rs to correctly attach headers to the hyper::Request.
  2. Client: Update OsisClient in crates/openrpc_http_client_lib/src/lib.rs to default to the /rpc endpoint.
  3. Tests: Include integration tests to verify end-to-end header transmission over UDS.

This is a breaking change for services expecting the legacy path, but it is necessary for alignment with the hero_context specification.

## Summary While investigating `hero_biz` issue #37, a fundamental issue was discovered in `hero_rpc`'s `OsisClient` and `OpenRpcTransport` that breaks context selection across the Hero OS ecosystem. ## Root Causes ### 1. URL Path Construction (Layer 1) `OsisClient::new` (Native) and `endpoint` (WASM) incorrectly construct the RPC endpoint using legacy per-domain or context-prefixed paths (e.g., `{base_url}/hero_osis_{domain}/rpc`). **Requirement:** Per `hero_context` and the Unified OSIS Server model, the endpoint must be a simple `{base_url}/rpc`. Routing is handled via method prefixes and the `X-Hero-Context` header. ### 2. Header Forwarding on UDS (Layer 2) The `OpenRpcTransport` implementation for Unix Domain Sockets (UDS) was found to drop HTTP headers. A comment in `transport.rs` stated: *"Unix-socket transport currently drops the headers — raw socket framing has no place to carry them"*. **Correction:** Since we use HTTP-over-UDS, we **can and must** carry headers like `X-Hero-Context` and `X-Hero-Claims` in the HTTP request framing over the socket. These headers are mandatory for context isolation and claim-based authorization. ## Impact All services using `hero_rpc` for context-aware OSIS calls are currently failing to transmit the context selector to the server, resulting in "silent" failures where all contexts return the same (default) data. ## Proposed Fix 1. **Transport:** Update `http_post_unix` and `post_raw_json_with_headers` in `crates/openrpc/src/transport.rs` to correctly attach headers to the `hyper::Request`. 2. **Client:** Update `OsisClient` in `crates/openrpc_http_client_lib/src/lib.rs` to default to the `/rpc` endpoint. 3. **Tests:** Include integration tests to verify end-to-end header transmission over UDS. This is a breaking change for services expecting the legacy path, but it is necessary for alignment with the `hero_context` specification.
Author
Member

@despiegk Can you check this? Is this something I should fix or just keep a local patch for hero_biz?

@despiegk Can you check this? Is this something I should fix or just keep a local patch for hero_biz?
Author
Member

Correction based on Timur's clarification

After discussing with Timur, Layer 1 of this issue is incorrect — the per-domain socket paths are not legacy, they are intentional architecture.

What the per-domain paths actually are

OSIS is a single backend server that handles many distinct data domains (calendar, contacts, business, messaging, etc.). Because each domain has dozens of its own RPC methods, a single shared OpenRPC spec would easily reach 3000+ methods, making it unworkable for AI tooling and hard to navigate. Kristof specifically designed OSIS to bind one socket per domain (e.g. hero_osis_business/rpc, hero_osis_calendar/rpc) so that:

  • Each domain exposes a small, domain-scoped OpenRPC spec
  • hero_router can treat each domain as a separate discoverable service
  • AI agents only see the methods relevant to the domain they are working against

This is current, active architecture — not a legacy pattern to be removed.

What WAS legacy (and already fixed)

The actual legacy pattern was encoding context in the socket path, e.g. hero_osis_business_threefold/rpc. That approach was abandoned because it required re-binding every service to a new socket for each context, which was architecturally unsound. The replacement — passing context via the X-Hero-Context HTTP header — was already implemented in commit ed6e7eb3c0 (feat(OsisClient): per-domain URL routing + X-Hero-Context header).

I had confused domain-in-path (still correct) with context-in-path (was legacy, already removed).

What remains as a real bug

Layer 2 — Header Forwarding on UDS is the actual issue to investigate. The X-Hero-Context header must survive the Unix Domain Socket transport hop. If the hyper-over-UDS transport is stripping custom headers before the request reaches the OSIS server, context selection will silently fail regardless of how the URL is constructed. Timur is currently checking this.

Updated scope

This issue should be scoped to Layer 2 only: verify and fix that X-Hero-Context (and X-Hero-Claims) are correctly forwarded through the UDS transport in crates/openrpc/src/transport.rs.

## Correction based on Timur's clarification After discussing with Timur, **Layer 1 of this issue is incorrect** — the per-domain socket paths are not legacy, they are intentional architecture. ### What the per-domain paths actually are OSIS is a single backend server that handles many distinct data domains (calendar, contacts, business, messaging, etc.). Because each domain has dozens of its own RPC methods, a single shared OpenRPC spec would easily reach 3000+ methods, making it unworkable for AI tooling and hard to navigate. Kristof specifically designed OSIS to bind **one socket per domain** (e.g. `hero_osis_business/rpc`, `hero_osis_calendar/rpc`) so that: - Each domain exposes a small, domain-scoped OpenRPC spec - hero_router can treat each domain as a separate discoverable service - AI agents only see the methods relevant to the domain they are working against This is current, active architecture — not a legacy pattern to be removed. ### What WAS legacy (and already fixed) The actual legacy pattern was encoding **context** in the socket path, e.g. `hero_osis_business_threefold/rpc`. That approach was abandoned because it required re-binding every service to a new socket for each context, which was architecturally unsound. The replacement — passing context via the `X-Hero-Context` HTTP header — was already implemented in commit [`ed6e7eb3c0`](https://forge.ourworld.tf/lhumina_code/hero_rpc/commit/ed6e7eb3c0aca6e102d0c15b3cfbb6ba838462de) (`feat(OsisClient): per-domain URL routing + X-Hero-Context header`). I had confused domain-in-path (still correct) with context-in-path (was legacy, already removed). ### What remains as a real bug **Layer 2 — Header Forwarding on UDS** is the actual issue to investigate. The `X-Hero-Context` header must survive the Unix Domain Socket transport hop. If the hyper-over-UDS transport is stripping custom headers before the request reaches the OSIS server, context selection will silently fail regardless of how the URL is constructed. Timur is currently checking this. ### Updated scope This issue should be scoped to **Layer 2 only**: verify and fix that `X-Hero-Context` (and `X-Hero-Claims`) are correctly forwarded through the UDS transport in `crates/openrpc/src/transport.rs`.
casper-stevens changed title from bug/arch: OsisClient constructs wrong URL and transport drops headers on UDS to bug: OpenRPC UDS transport drops X-Hero-Context and X-Hero-Claims headers 2026-05-06 16:24:05 +00:00
Author
Member

Exact fix required (confirmed by reading the code)

Repo: lhumina_code/hero_rpc — file crates/openrpc/src/transport.rs

The bug

post_raw_json_with_headers (line 216) accepts extra_headers: &[(&str, &str)] and correctly applies them when the transport is HTTP. But when the transport is UnixSocket, it calls http_post_unix without passing the headers — they are silently dropped:

// UnixSocket branch — extra_headers never passed:
TransportType::UnixSocket { path, rpc_path } => {
    let resp_bytes = self.http_post_unix(path, rpc_path, &body_bytes).await?;  // <-- headers lost
    ...
}

http_post_unix (line 354) has no headers parameter and builds a hardcoded hyper::Request with only Content-Type and Host.

call_unix_http (line 331) also calls http_post_unix directly with no headers.

Exact changes needed

1. Add extra_headers parameter to http_post_unix:

async fn http_post_unix(
    &self,
    socket_path: &str,
    path: &str,
    body: &[u8],
    extra_headers: &[(&str, &str)],  // add
) -> Result<Vec<u8>, OpenRpcError>

2. Apply them in the Request::builder() chain inside http_post_unix:

let mut builder = hyper::Request::builder()
    .method(hyper::Method::POST)
    .uri(path)
    .header("Content-Type", "application/json")
    .header("Host", "localhost");
for (name, value) in extra_headers {
    builder = builder.header(*name, *value);
}
let req = builder
    .body(Full::new(Bytes::copy_from_slice(body)))
    .map_err(|e| OpenRpcError::Transport(format!("Failed to build request: {}", e)))?;

3. Update the UnixSocket branch in post_raw_json_with_headers to pass headers through:

TransportType::UnixSocket { path, rpc_path } => {
    let resp_bytes = self.http_post_unix(path, rpc_path, &body_bytes, extra_headers).await?;
    ...
}

4. Update call_unix_http (line 331) to pass empty headers:

let response_bytes = self
    .http_post_unix(socket_path, rpc_path, &body_bytes, &[])
    .await?;

No other files need to change. The public API of post_raw_json_with_headers stays the same — callers already pass headers there, they just never reached the socket.

## Exact fix required (confirmed by reading the code) Repo: `lhumina_code/hero_rpc` — file `crates/openrpc/src/transport.rs` ### The bug `post_raw_json_with_headers` (line 216) accepts `extra_headers: &[(&str, &str)]` and correctly applies them when the transport is HTTP. But when the transport is `UnixSocket`, it calls `http_post_unix` without passing the headers — they are silently dropped: ```rust // UnixSocket branch — extra_headers never passed: TransportType::UnixSocket { path, rpc_path } => { let resp_bytes = self.http_post_unix(path, rpc_path, &body_bytes).await?; // <-- headers lost ... } ``` `http_post_unix` (line 354) has no headers parameter and builds a hardcoded `hyper::Request` with only `Content-Type` and `Host`. `call_unix_http` (line 331) also calls `http_post_unix` directly with no headers. ### Exact changes needed **1. Add `extra_headers` parameter to `http_post_unix`:** ```rust async fn http_post_unix( &self, socket_path: &str, path: &str, body: &[u8], extra_headers: &[(&str, &str)], // add ) -> Result<Vec<u8>, OpenRpcError> ``` **2. Apply them in the `Request::builder()` chain inside `http_post_unix`:** ```rust let mut builder = hyper::Request::builder() .method(hyper::Method::POST) .uri(path) .header("Content-Type", "application/json") .header("Host", "localhost"); for (name, value) in extra_headers { builder = builder.header(*name, *value); } let req = builder .body(Full::new(Bytes::copy_from_slice(body))) .map_err(|e| OpenRpcError::Transport(format!("Failed to build request: {}", e)))?; ``` **3. Update the `UnixSocket` branch in `post_raw_json_with_headers` to pass headers through:** ```rust TransportType::UnixSocket { path, rpc_path } => { let resp_bytes = self.http_post_unix(path, rpc_path, &body_bytes, extra_headers).await?; ... } ``` **4. Update `call_unix_http` (line 331) to pass empty headers:** ```rust let response_bytes = self .http_post_unix(socket_path, rpc_path, &body_bytes, &[]) .await?; ``` No other files need to change. The public API of `post_raw_json_with_headers` stays the same — callers already pass headers there, they just never reached the socket.
mik-tf added this to the ACTIVE project 2026-05-06 17:32:07 +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_rpc#42
No description provided.