feat: Dynamic socket-name URL routing (/{service}/{socket_name}/*) #16

Closed
opened 2026-04-08 11:00:18 +00:00 by mahmoud · 2 comments
Owner

Problem

hero_proxy currently routes /{service}/* and picks a socket via find_socket_for() which prefers ui.sock over rpc.sock. There's no way to target a specific socket via URL.

For multi-node hero_compute, the explorer needs to reach rpc.sock and explorer_rpc.sock on remote nodes — not ui.sock.

Solution: Dynamic filesystem lookup

No hardcoded socket types. Just check if the second URL segment matches a .sock file in the service directory.

Given:

~/hero/var/sockets/hero_compute/
  rpc.sock
  ui.sock
  explorer_rpc.sock

Routing:

/hero_compute/rpc/*             → hero_compute/rpc.sock
/hero_compute/ui/*              → hero_compute/ui.sock
/hero_compute/explorer_rpc/*    → hero_compute/explorer_rpc.sock
/hero_compute/*                 → find_socket_for() fallback (current behavior)

The logic is one filesystem check:

fn resolve_socket(socket_dir: &Path, service: &str, second_segment: &str) -> Option<PathBuf> {
    let typed = socket_dir.join(service).join(format!("{}.sock", second_segment));
    if typed.exists() {
        Some(typed)
    } else {
        None // fall back to find_socket_for()
    }
}

URL Processing

  1. Extract first two path segments: /{service}/{socket_name}/{rest}
  2. Check if $HERO_SOCKET_DIR/{service}/{socket_name}.sock exists
  3. If yes: route to it, strip /{service}/{socket_name}, forward /{rest}
  4. If no: fall back to current single-segment routing (/{service}/*find_socket_for())

Prefix header: X-Forwarded-Prefix: /{service}/{socket_name}

Why Dynamic

  • Services create arbitrary socket names (e.g. explorer_rpc.sock) that don't fit predefined types
  • No configuration needed — any .sock file is automatically routable
  • New sockets become available without restarting hero_proxy (scanner refreshes periodically)
  • Backward compatible — single-segment URLs still work via fallback

Examples

GET /hero_compute/rpc/api/root/cloud/rpc
  → exists: hero_compute/rpc.sock ✓
  → forward: /api/root/cloud/rpc

GET /hero_compute/explorer_rpc/api/root/explorer/rpc
  → exists: hero_compute/explorer_rpc.sock ✓
  → forward: /api/root/explorer/rpc

GET /hero_compute/something
  → exists: hero_compute/something.sock ✗
  → fallback: find_socket_for("hero_compute")

GET /hero_proc/rpc/some/method
  → exists: hero_proc/rpc.sock ✓
  → forward: /some/method
  • hero_compute#83 — multi-node connectivity depends on this
## Problem hero_proxy currently routes `/{service}/*` and picks a socket via `find_socket_for()` which prefers `ui.sock` over `rpc.sock`. There's no way to target a specific socket via URL. For multi-node hero_compute, the explorer needs to reach `rpc.sock` and `explorer_rpc.sock` on remote nodes — not `ui.sock`. ## Solution: Dynamic filesystem lookup No hardcoded socket types. Just check if the second URL segment matches a `.sock` file in the service directory. Given: ``` ~/hero/var/sockets/hero_compute/ rpc.sock ui.sock explorer_rpc.sock ``` Routing: ``` /hero_compute/rpc/* → hero_compute/rpc.sock /hero_compute/ui/* → hero_compute/ui.sock /hero_compute/explorer_rpc/* → hero_compute/explorer_rpc.sock /hero_compute/* → find_socket_for() fallback (current behavior) ``` The logic is one filesystem check: ```rust fn resolve_socket(socket_dir: &Path, service: &str, second_segment: &str) -> Option<PathBuf> { let typed = socket_dir.join(service).join(format!("{}.sock", second_segment)); if typed.exists() { Some(typed) } else { None // fall back to find_socket_for() } } ``` ## URL Processing 1. Extract first two path segments: `/{service}/{socket_name}/{rest}` 2. Check if `$HERO_SOCKET_DIR/{service}/{socket_name}.sock` exists 3. If yes: route to it, strip `/{service}/{socket_name}`, forward `/{rest}` 4. If no: fall back to current single-segment routing (`/{service}/*` → `find_socket_for()`) Prefix header: `X-Forwarded-Prefix: /{service}/{socket_name}` ## Why Dynamic - Services create arbitrary socket names (e.g. `explorer_rpc.sock`) that don't fit predefined types - No configuration needed — any `.sock` file is automatically routable - New sockets become available without restarting hero_proxy (scanner refreshes periodically) - Backward compatible — single-segment URLs still work via fallback ## Examples ``` GET /hero_compute/rpc/api/root/cloud/rpc → exists: hero_compute/rpc.sock ✓ → forward: /api/root/cloud/rpc GET /hero_compute/explorer_rpc/api/root/explorer/rpc → exists: hero_compute/explorer_rpc.sock ✓ → forward: /api/root/explorer/rpc GET /hero_compute/something → exists: hero_compute/something.sock ✗ → fallback: find_socket_for("hero_compute") GET /hero_proc/rpc/some/method → exists: hero_proc/rpc.sock ✓ → forward: /some/method ``` ## Related - hero_compute#83 — multi-node connectivity depends on this
mahmoud changed title from feat: Add socket-type URL routing (/{service}/{socket_type}/*) to feat: Dynamic socket-name URL routing (/{service}/{socket_name}/*) 2026-04-08 11:06:31 +00:00
Author
Owner

Current Behavior vs Needed Behavior

Current

URL format: /{service}/*

hero_proxy extracts the first path segment as the service name and calls find_socket_for(service) which searches in this priority order:

  1. ui.sock (human-facing dashboard)
  2. web_*.sock (first alphabetically)
  3. rest.sock
  4. rpc.sock
  5. Legacy flat sockets

This means every request to a service always lands on ui.sock if it exists, regardless of what the caller actually wants.

Example — the problem:

GET /hero_compute/api/root/cloud/rpc
  → find_socket_for("hero_compute")
  → finds ui.sock first (priority 1)
  → forwards to ui.sock ← WRONG, caller wanted rpc.sock

There is no way for a remote node to reach rpc.sock or explorer_rpc.sock through hero_proxy.

Needed

URL format: /{service}/{socket_name}/* with fallback to /{service}/*

hero_proxy extracts the first TWO path segments. If $HERO_SOCKET_DIR/{service}/{socket_name}.sock exists on disk, route to it directly. Otherwise fall back to current find_socket_for() behavior.

Example — the fix:

GET /hero_compute/rpc/api/root/cloud/rpc
  → check: hero_compute/rpc.sock exists? YES
  → forward to rpc.sock with path /api/root/cloud/rpc ← CORRECT

GET /hero_compute/explorer_rpc/api/root/explorer/rpc  
  → check: hero_compute/explorer_rpc.sock exists? YES
  → forward to explorer_rpc.sock ← CORRECT

GET /hero_compute/ui/dashboard
  → check: hero_compute/ui.sock exists? YES
  → forward to ui.sock ← CORRECT

GET /hero_compute/something
  → check: hero_compute/something.sock exists? NO
  → fallback: find_socket_for("hero_compute") → ui.sock ← backward compatible

This is fully backward compatible — existing single-segment URLs still work. New two-segment URLs give callers explicit control over which socket to reach.

## Current Behavior vs Needed Behavior ### Current URL format: `/{service}/*` hero_proxy extracts the first path segment as the service name and calls `find_socket_for(service)` which searches in this priority order: 1. `ui.sock` (human-facing dashboard) 2. `web_*.sock` (first alphabetically) 3. `rest.sock` 4. `rpc.sock` 5. Legacy flat sockets This means **every request to a service always lands on `ui.sock`** if it exists, regardless of what the caller actually wants. **Example — the problem:** ``` GET /hero_compute/api/root/cloud/rpc → find_socket_for("hero_compute") → finds ui.sock first (priority 1) → forwards to ui.sock ← WRONG, caller wanted rpc.sock ``` There is no way for a remote node to reach `rpc.sock` or `explorer_rpc.sock` through hero_proxy. ### Needed URL format: `/{service}/{socket_name}/*` with fallback to `/{service}/*` hero_proxy extracts the first TWO path segments. If `$HERO_SOCKET_DIR/{service}/{socket_name}.sock` exists on disk, route to it directly. Otherwise fall back to current `find_socket_for()` behavior. **Example — the fix:** ``` GET /hero_compute/rpc/api/root/cloud/rpc → check: hero_compute/rpc.sock exists? YES → forward to rpc.sock with path /api/root/cloud/rpc ← CORRECT GET /hero_compute/explorer_rpc/api/root/explorer/rpc → check: hero_compute/explorer_rpc.sock exists? YES → forward to explorer_rpc.sock ← CORRECT GET /hero_compute/ui/dashboard → check: hero_compute/ui.sock exists? YES → forward to ui.sock ← CORRECT GET /hero_compute/something → check: hero_compute/something.sock exists? NO → fallback: find_socket_for("hero_compute") → ui.sock ← backward compatible ``` This is fully backward compatible — existing single-segment URLs still work. New two-segment URLs give callers explicit control over which socket to reach.
mahmoud self-assigned this 2026-04-08 11:29:15 +00:00
mahmoud added this to the ACTIVE project 2026-04-08 11:29:17 +00:00
mahmoud added this to the now milestone 2026-04-08 11:29:19 +00:00
Author
Owner

Implementation Summary

Changes Made

crates/hero_proxy_server/src/config.rs

  • Added find_socket_by_name(service, socket_name) — direct filesystem lookup for $HERO_SOCKET_DIR/{service}/{socket_name}.sock

crates/hero_proxy_server/src/proxy.rs

  • Added extract_two_segment_prefix() — parses /{service}/{socket_name}/{rest} from URL path
  • Updated proxy_handler — tries two-segment routing first (checks if .sock file exists on disk), falls back to existing find_socket_for() single-segment routing

crates/hero_proxy_tests/src/lib.rs

  • Fixed create_mock_backend() to use per-service directory layout ({service}/rpc.sock) instead of flat legacy layout ({service}.sock)

Test Results

  • Build: Pass
  • Lint: Pass (clippy with -D warnings)
  • Tests: 97 passed, 0 failed, 8 ignored

PR

#17

Implementation committed: 4918fc3

## Implementation Summary ### Changes Made **`crates/hero_proxy_server/src/config.rs`** - Added `find_socket_by_name(service, socket_name)` — direct filesystem lookup for `$HERO_SOCKET_DIR/{service}/{socket_name}.sock` **`crates/hero_proxy_server/src/proxy.rs`** - Added `extract_two_segment_prefix()` — parses `/{service}/{socket_name}/{rest}` from URL path - Updated `proxy_handler` — tries two-segment routing first (checks if `.sock` file exists on disk), falls back to existing `find_socket_for()` single-segment routing **`crates/hero_proxy_tests/src/lib.rs`** - Fixed `create_mock_backend()` to use per-service directory layout (`{service}/rpc.sock`) instead of flat legacy layout (`{service}.sock`) ### Test Results - **Build**: ✅ Pass - **Lint**: ✅ Pass (clippy with `-D warnings`) - **Tests**: 97 passed, 0 failed, 8 ignored ### PR https://forge.ourworld.tf/lhumina_code/hero_proxy/pulls/17 Implementation committed: `4918fc3`
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_proxy#16
No description provided.