# How to Implement Custom Protocol Handlers Using the ProtocolHandler Trait in iroh

> Learn to implement custom protocol handlers in iroh using the ProtocolHandler trait. Define your struct, register it with Router for specific ALPN identifiers, and extend iroh's routing capabilities.

- Repository: [number zero/iroh](https://github.com/n0-computer/iroh)
- Tags: how-to-guide
- Published: 2026-07-14

---

**The iroh library routes incoming QUIC connections based on Application-Layer Protocol Negotiation (ALPN) strings, enabling you to implement custom protocol handlers by defining a struct that implements the `ProtocolHandler` trait and registering it with a `Router` for your specific ALPN identifier.**

The iroh networking stack (n0-computer/iroh) provides a modular architecture for building peer-to-peer applications over QUIC. By implementing the `ProtocolHandler` trait, you define how your application handles incoming connections, processes bidirectional streams, and manages protocol-specific lifecycle events. This approach separates transport concerns from application logic, allowing the `Router` to manage connection dispatch while your handler implements the protocol semantics.

## Core Architecture Components

### Router and ProtocolMap

At the heart of iroh's protocol system is the **`Router`**, which owns an `Endpoint` and maintains a `ProtocolMap` to dispatch incoming connections. As implemented in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the `RouterBuilder` configures this mapping via the `.accept(alpn, handler)` method, associating ALPN byte strings with boxed protocol handlers before calling `.spawn()` to create the running router instance.

The **`ProtocolMap`** stores `Box<dyn DynProtocolHandler>` instances keyed by ALPN identifiers. When a peer connects, the router looks up the handler using `get(&alpn)` and delegates connection management to the corresponding implementation.

### DynProtocolHandler and Connection Flow

**`DynProtocolHandler`** represents the dyn-compatible version of the `ProtocolHandler` trait, returning boxed futures to enable trait object usage. According to the source code in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), the connection handling flow follows this sequence:

1. The router's `handle_connection` loop receives an `Incoming` connection and validates the ALPN
2. The router calls `handler.on_accepting` (optional hook for early validation or 0-RTT handling)
3. Upon success, the router spawns a Tokio task to run `handler.accept(connection)`, allowing long-running protocols to execute concurrently
4. During shutdown, the router invokes each handler's `shutdown` method concurrently before aborting remaining tasks

## Implementing the ProtocolHandler Trait

To create a custom protocol handler, define a type (typically a zero-size struct) and implement the `ProtocolHandler` trait with the following requirements:

- **Send + Sync + 'static** bounds for thread-safe sharing across async tasks
- **Debug** implementation for logging and diagnostics
- **`accept`** method – the required async fn that receives a `Connection` and implements your protocol logic

### Optional Lifecycle Hooks

**`on_accepting`** – Override this method to perform early validation before the connection is fully accepted, or to handle 0-RTT data. The default implementation simply resolves the `Accepting` future.

**`shutdown`** – Implement this method to clean up resources, close lingering connections, or persist state when the router initiates graceful shutdown. The router awaits all shutdown futures concurrently.

### Method Signature

```rust
use iroh::protocol::{ProtocolHandler, AcceptError};
use iroh::endpoint::Connection;

#[derive(Debug, Clone)]
struct MyHandler;

impl ProtocolHandler for MyHandler {
    async fn accept(&self, connection: Connection) -> Result<(), AcceptError> {
        // Protocol implementation: open streams, read/write data
        Ok(())
    }
}

```

## Registering and Running Protocol Handlers

### ALPN Registration

Register your handler with the router using the builder pattern. The ALPN bytes must be unique across your application:

```rust
use iroh::{Endpoint, protocol::Router};
use iroh::endpoint::presets;

let endpoint = Endpoint::bind(presets::N0).await?;
let router = Router::builder(endpoint)
    .accept(b"/my/custom/1", MyHandler)
    .spawn();

```

The router spawns background tasks to accept incoming connections and dispatch them to the appropriate handler based on the ALPN negotiation.

### Graceful Shutdown

Always await `router.shutdown().await` before dropping the endpoint to ensure all protocol handlers receive the shutdown signal and can close connections cleanly:

```rust
router.shutdown().await?;

```

## Complete Implementation Examples

### Echo Protocol Example

This minimal implementation from [`iroh/examples/echo.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/echo.rs) demonstrates a complete handler that accepts bidirectional streams and echoes data back to the client:

```rust
use iroh::{
    Endpoint, EndpointAddr,
    endpoint::{Connection, presets},
    protocol::{AcceptError, ProtocolHandler, Router},
};
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)
}

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(())
}

```

### Custom Transport Integration

The example in [`iroh/examples/custom-transport.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/examples/custom-transport.rs) shows how protocol handlers work with custom transports and path selection:

```rust
use iroh::{
    Endpoint, SecretKey,
    endpoint::{Builder, Connection, presets},
    protocol::{AcceptError, ProtocolHandler, Router},
    test_utils::test_transport::{TEST_TRANSPORT_ID, TestNetwork, TestTransport},
};
use n0_error::Result;

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(())
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let network = TestNetwork::new();
    let s1 = SecretKey::from([0u8; 32]);
    let s2 = SecretKey::from([1u8; 32]);

    let t1 = network.create_transport(s1.public())?;
    let ep1 = Endpoint::builder().secret_key(s1).bind().await?;
    
    let t2 = network.create_transport(s2.public())?;
    let ep2 = Endpoint::builder().secret_key(s2).bind().await?;

    let server = Router::builder(ep2).accept(ALPN, Echo).spawn();
    let conn = ep1.connect(s2.public(), ALPN).await?;
    
    // Use connection...
    
    server.shutdown().await?;
    Ok(())
}

```

## Summary

- **Implement `ProtocolHandler`** by defining the required `accept` method and optionally `on_accepting` and `shutdown` for lifecycle management.
- **Register handlers** using `Router::builder(endpoint).accept(alpn_bytes, handler).spawn()` to map ALPN identifiers to your implementation.
- **Connection dispatch** occurs automatically via `ProtocolMap` in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), which looks up handlers based on the ALPN string negotiated during the QUIC handshake.
- **Graceful shutdown** requires awaiting `router.shutdown()` to ensure all handlers cleanup resources before the endpoint closes.
- **Trait requirements** include `Send + Sync + 'static` and `Debug`, with the `accept` method running in its own Tokio task to support long-running protocols.

## Frequently Asked Questions

### What is the difference between ProtocolHandler and DynProtocolHandler?

**`ProtocolHandler`** is the generic trait you implement for your custom protocols, while **`DynProtocolHandler`** is the dyn-compatible object-safe version used internally by the `ProtocolMap`. As defined in [`iroh/src/protocol.rs`](https://github.com/n0-computer/iroh/blob/main/iroh/src/protocol.rs), `DynProtocolHandler` wraps your concrete type in a box and returns boxed futures, enabling the router to store heterogeneous handler types in a single map keyed by ALPN strings.

### How does ALPN routing work in iroh?

When a peer initiates a QUIC connection, the TLS handshake includes an ALPN identifier. The router's `handle_connection` method extracts this string and queries the `ProtocolMap` using `get(&alpn)`. If a matching handler exists, the router calls your implementation; otherwise, the connection is rejected. This mechanism allows multiple protocols to coexist on a single endpoint.

### Can a single router instance handle multiple protocol handlers?

Yes. The `RouterBuilder` accepts multiple registrations via repeated `.accept()` calls, allowing you to serve different protocols on the same endpoint. Each handler operates independently with its own ALPN identifier, and the router dispatches connections to the appropriate handler based on the negotiated protocol string.

### How do I handle errors within the accept method?

The `accept` method returns `Result<(), AcceptError>`. If your protocol encounters an unrecoverable error, return `Err(AcceptError::other(e))` or the appropriate variant. The router logs the error and closes the connection. For recoverable errors within your protocol logic (such as stream read failures), handle them internally within the `accept` future before returning `Ok(())` when the protocol completes normally.