Skip to content

Support self-requests (a node fetching its own node ID) via in-process loopback #264

Description

@momics

Summary

A node cannot currently fetch() (or dial()) its own node ID. For plain HTTP it is normal and expected for a server to make a request to itself (e.g. curl http://127.0.0.1:port on the same host), so users reasonably expect node.fetch("httpi://<own-node-id>/…") to work when the same node both serves and fetches. Today it fails.

The root cause is upstream: iroh's transport hard-blocks self-dial. We can't change that, but we can honor the HTTP intuition by providing an application-level loopback at the iroh-http layer — analogous to how the OS loopback interface lets a TCP server talk to itself without traversing the NIC.

Evidence

A throwaway probe (single endpoint, serving, fetching its own node ID over its own direct addrs) returns an explicit upstream error:

PROBE own_id=7lysj7wkal6yody3g5s4qksk4nbtdpv4ynfgf5m7ch5dnh2shikq addrs=[127.0.0.1:58473]
PROBE RESULT: ERROR — CoreError { code: ConnectionFailed,
  message: "connect: Connecting to ourself is not supported" }

The message "Connecting to ourself is not supported" is emitted by iroh's Endpoint::connect, not by iroh-http. There is no self-dial guard anywhere in iroh-http-core — the failure originates one layer down. All existing connection tests use two distinct endpoints via common::make_pair() (crates/iroh-http-core/tests/common/mod.rs); none exercises self-targeting.

Impact

  • Who: anyone running a single-node app/demo/test that wants the same node to serve and consume its own endpoints (very common for local development, examples, smoke tests).
  • Severity: low — there is a workaround (run two endpoints / two processes), but it's surprising friction and contradicts HTTP intuition.
  • Concretely, it blocks a one-click local demo of the Tauri httpi:// scheme handler (a <audio>/<img> element pointed at the app's own server), which is what surfaced this.

Remediation (proposed design)

Provide an in-process loopback at the core layer instead of asking iroh to self-dial (which it refuses):

  1. Store a reference to the locally-registered serve handler (or a dispatch channel) on EndpointInner when ffi_serve is called.
  2. In fetch(), detect target_node_id == own_node_id and, instead of going through the connection pool / QUIC, dispatch the request directly to the stored serve handler using the existing req_handle / res_body_handle machinery.
  3. If no local server is registered, return a clear error (e.g. ConnectionFailed / a dedicated code) rather than the opaque upstream message.

Notes:

  • This works uniformly across adapters (Node/Deno/Tauri): core simply invokes the stored handler closure, which is the adapter's existing bridge into the JS serve callback. The Tauri httpi:// scheme handler routes through core fetch, so self-audio/self-image would "just work" with no per-adapter code.
  • peer-id for a loopback request would be the node's own ID — truthful (the peer is us); document this explicitly.

Tradeoffs / open questions

  • The loopback path is not the real network path: no QUIC/TLS handshake, no wire compression, no connection pool, different latency. Same caveat as TCP loopback skipping the NIC — convenient for local use, but a self-request must not be mistaken for a network-reachability check. Document clearly.
  • This changes a documented invariant ("a connection is always between two distinct peers"), so it warrants an ADR referencing this probe evidence (per the repo's Trace principle).
  • Should dial() (raw QUIC sessions) get the same loopback treatment, or only fetch()? Sessions bridging (bidi streams, datagrams, backpressure, cancellation) is where subtle bugs live — possibly a follow-up.
  • Should loopback be implicit (automatic on self-target) or opt-in? Implicit matches HTTP intuition; opt-in is safer against accidental reliance.

Acceptance criteria

  1. node.fetch("httpi://<own-node-id>/path") succeeds when the same node has an active server, routing to the local serve handler without a QUIC connection.
  2. When no local server is registered, the self-fetch returns a clear, documented error (not the opaque upstream "Connecting to ourself is not supported").
  3. Behavior is consistent across Node, Deno, and Tauri adapters (covered by an FFI-boundary test in adapter.test.ts / adapter.test.mjs).
  4. A Rust core regression test exercises serve + self-fetch on a single endpoint.
  5. An ADR documents the decision, the upstream constraint (with this probe evidence), and the loopback semantics (including peer-id).
  6. Docs/specification updated to describe self-requests and the "not a network path" caveat.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Low priorityapiAPI design / ergonomicsconnectivityPeer discovery and connectiondxDeveloper experienceenhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions