Using Bi-Directional QUIC Streams in iroh: Implementation Guide
Bi-directional QUIC streams in iroh provide full-duplex communication channels where each peer holds a SendStream and RecvStream, accessed via Connection::open_bi and Connection::accept_bi in src/endpoint/connection.rs.
iroh is a peer-to-peer networking library built on the QUIC protocol (via the noq crate) that abstracts complex connection handling into ergonomic async APIs. Working with bi-directional QUIC streams allows you to establish simultaneous read and write channels over a single connection without head-of-line blocking. This guide explores the core API methods, critical deadlock warnings, and implementation patterns found in the n0-computer/iroh repository.
Core API for Bi-Directional QUIC Streams
The primary interface for bi-directional stream management resides in src/endpoint/connection.rs. The Connection type exposes two essential async methods for stream establishment that return futures resolving to (SendStream, RecvStream) tuples.
Initiating Outgoing Streams with open_bi
To create a new bi-directional stream from the client side, call Connection::open_bi. This method returns an OpenBi future that resolves to a pair of streams for full-duplex communication.
let (mut send, mut recv) = conn.open_bi().await?;
Accepting Incoming Streams with accept_bi
On the server side, Connection::accept_bi waits for the next incoming bi-directional stream request. This method returns an AcceptBi future with the same stream tuple structure.
let (mut send, mut recv) = conn.accept_bi().await?;
Critical Deadlock Warning
A crucial implementation detail in src/endpoint/connection.rs dictates that the initiating peer must write to its SendStream before the remote peer can successfully read from its corresponding RecvStream. Calling open_bi and immediately awaiting the receive operation without writing data will deadlock the connection. This behavior stems from QUIC's stream flow-control handshake requirements and is explicitly documented in the source comments surrounding these methods.
Managing Stream Lifecycle
Proper cleanup ensures reliable QUIC connection management and prevents resource leaks.
Closing Send Streams with finish
Signal end-of-data by calling SendStream::finish, defined in src/endpoint.rs. This notifies the remote receiver that no more data will be sent.
send.write_all(b"request data").await?;
send.finish()?; // Signals EOF to remote receiver
Monitoring Connection Termination
Await complete connection shutdown using Connection::closed, also located in src/endpoint.rs:
conn.close(quic::VarInt::from_u32(0), b"normal shutdown");
conn.closed().await;
Practical Implementation Examples
Client-Side Stream Pattern
The client implementation in examples/search.rs demonstrates the complete workflow: open, write, finish, then read. This pattern ensures the deadlock rule is satisfied by writing immediately after opening.
let (mut send, mut recv) = conn.open_bi().await?;
send.write_all(b"search query").await?;
send.finish()?;
let mut response = Vec::new();
recv.read_to_end(&mut response).await?;
Server-Side Stream Pattern
The echo server in examples/echo.rs shows the acceptance and echo pattern using tokio::io::copy to stream data between the receive and send sides.
let (mut send, mut recv) = conn.accept_bi().await?;
tokio::io::copy(&mut recv, &mut send).await?;
send.finish()?;
QUIC Integration in iroh's Architecture
The QUIC transport layer is configured through iroh::endpoint::Endpoint in src/lib.rs. When providing Options::quic_config, iroh constructs QuicClientConfig and QuicServerConfig instances that power the underlying noq crate connections. The Connection type serves as a thin wrapper around noq::Connection, exposing the open_bi and accept_bi helpers while inheriting QUIC's built-in flow-control, congestion control, and loss-recovery guarantees.
Summary
- Bi-directional streams provide full-duplex communication via
SendStreamandRecvStreampairs managed insrc/endpoint/connection.rs. - Deadlock prevention requires the initiator to write data before the remote can receive, as documented in the source comments.
- API entry points are
Connection::open_bifor clients andConnection::accept_bifor servers. - Lifecycle management uses
SendStream::finishto signal EOF andConnection::closedto await termination. - Real-world examples are available in
examples/echo.rsandexamples/search.rswithin the n0-computer/iroh repository.
Frequently Asked Questions
What is the difference between open_bi and accept_bi in iroh?
open_bi initiates a new outgoing bi-directional stream from the client side, while accept_bi waits for and accepts incoming stream requests on the server side. Both methods return a tuple of (SendStream, RecvStream) but are used by opposite connection roles according to the implementation in src/endpoint/connection.rs.
Why does my iroh application deadlock when using bi-directional streams?
Deadlocks occur when the initiating peer calls open_bi but fails to write data to the SendStream before the remote peer attempts to read. The source code in src/endpoint/connection.rs explicitly documents this requirement: you must write before the remote can successfully await the receive stream, as the stream establishment requires data transmission to complete the handshake.
How do I properly close a bi-directional QUIC stream in iroh?
Call SendStream::finish to signal end-of-file to the remote peer, allowing the receiving side to exit its read loop. Once all streams are closed, use Connection::close followed by Connection::closed to gracefully terminate the underlying QUIC connection and await complete shutdown.
Where can I find working examples of bi-directional stream usage?
The n0-computer/iroh repository contains examples/echo.rs, which demonstrates a server echoing data back over bi-directional streams, and examples/search.rs, which shows a client sending requests and reading responses. Both files illustrate proper open-write-finish-read patterns and are referenced in the crate-level documentation in src/lib.rs.
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 →