How to Open a QUIC Stream in Iroh: Methods and Examples

You open a QUIC stream in Iroh by calling open_uni() for unidirectional (send-only) streams or open_bi() for bidirectional streams on an established Connection, awaiting the resulting future to obtain the stream object, and using standard AsyncWrite or AsyncRead traits to transfer data.

Iroh's networking layer is built on top of the noq QUIC implementation, providing async-first APIs for peer-to-peer communication in the n0-computer/iroh repository. After establishing a connection between endpoints, you must open a QUIC stream in Iroh to transmit application data, choosing between unidirectional or bidirectional channels based on your protocol requirements.

Prerequisites: Obtaining a Connection

Before you can open a QUIC stream in Iroh, you need an active Connection handle. This is obtained either by connecting to a remote node or accepting an incoming connection handshake.

use iroh::Endpoint;

// Client side: connect to a remote server
let endpoint = Endpoint::client("0.0.0.0:0".parse()?)?;
let conn = endpoint.connect(server_addr, "example.com")
    .await?                 // performs the QUIC handshake
    .await?;                // yields a `Connection`

The Connection struct is defined in iroh/src/endpoint/connection.rs and provides the methods for stream creation.

Opening Outgoing Streams

The iroh/src/endpoint/connection.rs file implements two primary methods for creating new outgoing streams. Both return futures that resolve when the remote peer acknowledges the stream creation.

Unidirectional Streams with open_uni()

Use open_uni() to create a send-only stream where the peer can only read from the returned object. According to the source at lines 11211-11215, this method returns an OpenUni future that resolves to a stream implementing AsyncWrite.

use iroh::endpoint::ConnectionExt; // for `.anyerr()`
use tokio::io::AsyncWriteExt;

// Open a unidirectional stream
let mut send_stream = conn.open_uni().await.anyerr()?;

// Write data and close the sending side
send_stream.write_all(b"hello iroh").await?;
send_stream.finish().await?;

Bidirectional Streams with open_bi()

Use open_bi() to create a read-write stream where you can both send and receive data. This returns an OpenBi future that resolves to a tuple of (SendStream, RecvStream).

use tokio::io::{AsyncReadExt, AsyncWriteExt};

// Open a bidirectional stream
let (mut send, mut recv) = conn.open_bi().await.anyerr()?;

// Send request
send.write_all(b"ping").await?;
send.finish().await?;

// Read response
let mut buf = vec![0u8; 64];
let n = recv.read(&mut buf).await?;

Accepting Incoming Streams (Server Side)

When acting as a server, you accept connections and then wait for the remote peer to open streams using the accept_uni() and accept_bi() methods. The implementation in iroh/src/endpoint/connection.rs (around lines 11289-11295) provides these counterparts to the open methods.

// Accept an incoming connection
let conn = server_endpoint.accept().await
    .ok_or_else(|| anyhow::anyhow!("no incoming connection"))?
    .await?;

// Accept the next unidirectional stream opened by the client
let mut recv = conn.accept_uni().await.anyerr()?;

// Read the payload
let mut data = Vec::new();
recv.read_to_end(&mut data).await?;

Error Handling and Stream Limits

When you open a QUIC stream in Iroh, the operation may fail if the peer has reached its stream limit or refuses new streams. The examples above use the anyerr() helper method, provided by iroh::endpoint::ConnectionExt and defined in iroh/src/util.rs, to convert noq::Error into a standard Result type.

Both open_uni() and open_bi() return futures that can fail with connection errors if the underlying QUIC connection closes before the stream is established. Always await these futures within your error handling context.

Complete Working Example

The official Iroh example in iroh/examples/remote-info.rs demonstrates a server opening a unidirectional stream to send data. Here is the excerpt showing the key pattern:

// iroh/examples/remote-info.rs (lines 68-73)
let mut s = conn.open_uni().await.anyerr()?; // wait for stream to open
tokio::time::sleep(Duration::from_millis(500)).await;
s.write_all(b"hi").await.anyerr()?;           // write data
s.finish().anyerr()?;                        // close the sending side

When combined with connection setup, the full client workflow to open a QUIC stream in Iroh looks like this:

use iroh::Endpoint;
use iroh::endpoint::ConnectionExt;
use noq::VarInt;
use tokio::io::AsyncWriteExt;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create endpoint and connect
    let client_ep = Endpoint::client("0.0.0.0:0".parse()?)?;
    let conn = client_ep.connect("127.0.0.1:9999".parse()?, "example.com")
        .await?.await?;

    // Open and use a unidirectional stream
    let mut stream = conn.open_uni().await.anyerr()?;
    stream.write_all(b"hello from iroh client").await?;
    stream.finish().await?;
    
    // Gracefully close connection
    conn.close(VarInt::from_u32(0), b"done");
    Ok(())
}

Summary

  • Choose stream direction: Use open_uni() for send-only streams and open_bi() for bidirectional communication.
  • Await the future: Both methods return futures (OpenUni and OpenBi) that must be awaited to obtain the actual stream objects.
  • Source location: The implementation lives in iroh/src/endpoint/connection.rs at lines 11211-11215.
  • Handle errors: Use the anyerr() helper from ConnectionExt to map noq errors into standard Results, or handle noq::Error directly.
  • Server counterpart: Use accept_uni() and accept_bi() to receive streams initiated by remote peers.

Frequently Asked Questions

What is the difference between unidirectional and bidirectional streams in Iroh?

Unidirectional streams (open_uni()) allow only the initiating peer to write data, while the receiving peer can only read. Bidirectional streams (open_bi()) provide two-way communication where both sides can read and write. Choose unidirectional for simple data pushes and bidirectional for request-response protocols.

How do I handle errors when opening a QUIC stream in Iroh?

The open_uni() and open_bi() methods return futures that may resolve to errors if the connection is closed or stream limits are exceeded. Use the .anyerr() helper method from iroh::endpoint::ConnectionExt to convert noq::Error into a standard Result type, allowing you to use the ? operator for ergonomic error propagation.

Can I open multiple streams on a single connection?

Yes, Iroh supports multiplexing multiple streams over a single QUIC connection. You can call open_uni() or open_bi() multiple times on the same Connection handle to create concurrent streams. Each stream operates independently with its own flow control, though they share the underlying connection's congestion control and security context.

Where can I find the implementation details for stream creation?

The primary implementation is in iroh/src/endpoint/connection.rs, specifically lines 11211-11215 for the open_uni() and open_bi() methods. For practical usage examples, see iroh/examples/remote-info.rs and iroh/examples/transfer.rs, which demonstrate both client and server patterns for stream handling.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →