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):
- Store a reference to the locally-registered serve handler (or a dispatch channel) on
EndpointInner when ffi_serve is called.
- 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.
- 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
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.
- When no local server is registered, the self-fetch returns a clear, documented error (not the opaque upstream
"Connecting to ourself is not supported").
- Behavior is consistent across Node, Deno, and Tauri adapters (covered by an FFI-boundary test in
adapter.test.ts / adapter.test.mjs).
- A Rust core regression test exercises serve + self-fetch on a single endpoint.
- An ADR documents the decision, the upstream constraint (with this probe evidence), and the loopback semantics (including
peer-id).
- Docs/specification updated to describe self-requests and the "not a network path" caveat.
Summary
A node cannot currently
fetch()(ordial()) 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:porton the same host), so users reasonably expectnode.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:
The message
"Connecting to ourself is not supported"is emitted by iroh'sEndpoint::connect, not by iroh-http. There is no self-dial guard anywhere iniroh-http-core— the failure originates one layer down. All existing connection tests use two distinct endpoints viacommon::make_pair()(crates/iroh-http-core/tests/common/mod.rs); none exercises self-targeting.Impact
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):
EndpointInnerwhenffi_serveis called.fetch(), detecttarget_node_id == own_node_idand, instead of going through the connection pool / QUIC, dispatch the request directly to the stored serve handler using the existingreq_handle/res_body_handlemachinery.ConnectionFailed/ a dedicated code) rather than the opaque upstream message.Notes:
httpi://scheme handler routes through corefetch, so self-audio/self-image would "just work" with no per-adapter code.peer-idfor a loopback request would be the node's own ID — truthful (the peer is us); document this explicitly.Tradeoffs / open questions
dial()(raw QUIC sessions) get the same loopback treatment, or onlyfetch()? Sessions bridging (bidi streams, datagrams, backpressure, cancellation) is where subtle bugs live — possibly a follow-up.Acceptance criteria
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."Connecting to ourself is not supported").adapter.test.ts/adapter.test.mjs).peer-id).