Why Iroh Examples Are Not Working: Common Fixes and Correct Usage
Iroh examples fail most often due to unreachable relay servers, incorrect Cargo invocation, or missing finish() calls on QUIC streams, yet run correctly when the listener starts before the client and network dependencies are configured.
The iroh crate from the n0-computer/iroh repository provides a peer-to-peer QUIC stack with built-in hole-punching and relay fallback. If your Iroh examples are not working, the issue typically stems from runtime dependencies or invocation order rather than broken source code. This guide explains the correct way to run the examples and how to resolve the most common failure modes.
Understanding the Iroh Architecture
Before troubleshooting, you need to understand the core components that the examples exercise. The Endpoint struct in iroh/src/lib.rs represents a local node capable of both dialing remote peers and accepting inbound connections. When you run an example, it typically constructs an Endpoint via Endpoint::bind(presets::N0).await, which connects to a home relay and begins listening.
The Router struct in iroh/src/protocol/mod.rs dispatches incoming connections to protocol handlers based on ALPN strings. The echo example demonstrates this by registering a handler that responds to the b"iroh-example/echo/0" ALPN identifier. Connections are authenticated automatically using the endpoint's permanent SecretKey, with the corresponding PublicKey (also called EndpointId) used for routing and TLS verification.
Common Failure Modes and Fixes
Most example failures fall into five categories. Use this reference to diagnose your specific error.
Relay Resolution Failures
If you encounter a panic stating "failed to resolve relay address", your network cannot reach the default relay at relay0.n0.computer on port 443. This prevents the endpoint from establishing its home relay connection, which is required for NAT traversal.
Fix this by setting the IROH_RELAY environment variable to a custom relay URL, or configure it programmatically:
use iroh::{Endpoint, endpoint::presets};
let endpoint = Endpoint::builder(presets::N0)
.relay_url("https://my-relay.example".parse()?)
.bind()
.await?;
Process Exits Immediately
When running cargo run --example echo, the process may build successfully but exit before any data transfers. This happens because the example spawns a router but the main task ends before the asynchronous runtime completes the connection handshake.
Ensure you await the connection lifecycle. In the echo example, the listener must call router.endpoint().online().await before the client attempts to connect, and the client should await conn.closed().await or keep the runtime alive until the transfer completes.
Missing Feature Flags
Compilation errors referencing unstable-net-report or other modules indicate the crate was built without required optional features. The examples may depend on unstable or experimental APIs that are not enabled by default.
Add the necessary features to your Cargo.toml:
[dependencies]
iroh = { version = "0.x", features = ["unstable-net-report"] }
TLS Handshake Failures
A TLS handshake fails when the peer's PublicKey is not recognized or the ALPN strings differ between client and server. In iroh/src/tls/mod.rs, the library validates that the remote endpoint's certificate matches the expected EndpointId.
Verify that both sides use the exact same ALPN byte string:
const ALPN: &[u8] = b"iroh-example/echo/0"; // Must match exactly on both sides
Hanging Stream Reads
If your stream read blocks forever, the sender likely never called finish(). In QUIC, the receiver waits for the stream to be closed before returning from read_to_end().
Always signal the end of data:
send.write_all(b"payload").await?;
send.finish()?; // Critical: without this, recv.read_to_end() hangs
let response = recv.read_to_end(1000).await?;
How to Run the Examples Correctly
The bundled examples in iroh/examples/ demonstrate proper usage patterns. Follow these steps to ensure they execute successfully.
Running the Echo Server and Client
The echo example in iroh/examples/echo.rs shows a complete round-trip. It requires running both a listener and a client in sequence.
Server implementation (registers the protocol handler):
use iroh::{
Endpoint,
protocol::{ProtocolHandler, Router, AcceptError},
endpoint::{Connection, presets},
};
use n0_error::{Result, StdResultExt};
const ALPN: &[u8] = b"iroh-example/echo/0";
#[derive(Debug, Clone)]
struct Echo;
impl ProtocolHandler for Echo {
async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
let (mut send, mut recv) = connection.accept_bi().await?;
tokio::io::copy(&mut recv, &mut send).await?;
send.finish()?;
connection.closed().await;
Ok(())
}
}
async fn start_accept_side() -> Result<Router> {
let endpoint = Endpoint::bind(presets::N0).await?;
let router = Router::builder(endpoint).accept(ALPN, Echo).spawn();
Ok(router)
}
Client implementation (dials the listener):
async fn connect_side(addr: EndpointAddr) -> Result<()> {
let endpoint = Endpoint::bind(presets::N0).await?;
let conn = endpoint.connect(addr, ALPN).await?;
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;
send.write_all(b"Hello, world!").await.anyerr()?;
send.finish().anyerr()?;
let response = recv.read_to_end(1000).await.anyerr()?;
assert_eq!(&response, b"Hello, world!");
conn.close(0u32.into(), b"bye!");
endpoint.close().await;
Ok(())
}
Execution order:
- Start the listener with
router.endpoint().online().awaitto ensure it is reachable - Retrieve the advertised address via
router.endpoint().addr() - Pass this address to the client side
- Call
router.shutdown().awaitafter the client completes
Running the Blob Transfer Example
The transfer example in iroh/examples/transfer.rs demonstrates blob storage using the iroh-blobs crate:
use iroh_blobs::BlobClient;
use iroh::{Endpoint, endpoint::presets};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let ep = Endpoint::bind(presets::N0).await?;
let client = BlobClient::new(ep.clone());
let data = b"The quick brown fox jumps over the lazy dog";
let hash = client.put(data.as_ref()).await?;
let fetched = client.get(&hash).await?.read_to_end(usize::MAX).await?;
assert_eq!(fetched, data);
Ok(())
}
This example requires the iroh-blobs dependency and a running endpoint, but does not require a separate listener since it operates on the local node.
Key Source Files for Debugging
When tracing through failures, consult these implementation files:
iroh/src/lib.rs— ExportsEndpoint,RelayUrl, andEndpointIdiroh/src/endpoint/mod.rs— ContainsEndpoint::bind()and connection handlingiroh/src/protocol/mod.rs— ImplementsRouterandProtocolHandlertraitsiroh-relay/src/server/http_server.rs— Relay server implementation for fallback transportiroh-dns/src/dns.rs— DNS-based address lookup via Pkarr
Summary
- Iroh examples not working usually indicates missing relay connectivity, incorrect feature flags, or improper async runtime handling rather than code bugs
- Always start the listener before the client and wait for
endpoint.online().await - Ensure ALPN strings match exactly between connecting peers to avoid TLS failures
- Call
send.finish()on all streams to prevent receiver hangs - Set
IROH_RELAYor useEndpoint::builder().relay_url()if the default relay is unreachable
Frequently Asked Questions
Why does the echo example exit immediately without printing anything?
The example process exits because the main async task completes before the connection handshake finishes. The listener must await router.endpoint().online().await to ensure the node is reachable, and the client must keep the runtime alive until conn.closed().await resolves. Use tokio::join! to run both sides concurrently in a single process, or run the listener in a separate terminal first.
How do I fix "failed to resolve relay address" panics?
This error occurs when the default relay at relay0.n0.computer is unreachable due to network restrictions or DNS blocking. Export the IROH_RELAY environment variable with an alternative URL, or configure the endpoint builder with .relay_url("https://custom-relay.example") before calling .bind().await.
Why does my stream read hang forever even though data was sent?
The receiver blocks because QUIC streams require an explicit end-of-stream signal. The sender must call send.finish().await (or send.finish()? depending on your error handling) after writing the final bytes. Without this call, recv.read_to_end() waits indefinitely for more data.
Do I need to run my own relay server to use the examples?
No, the examples work out-of-the-box with the default public relay infrastructure provided by n0.computer. However, if you are in a restricted network environment, you may need to deploy your own relay server using the code in iroh-relay/src/server/http_server.rs and point your endpoint to it via the relay URL configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →