n0-computer/iroh · error · ConnectWithOptsError

InvalidAlpn

InvalidAlpn

Error message

Invalid ALPN

What it means

The ALPN bytes passed to endpoint.connect were empty. ALPN (application-layer protocol negotiation) is mandatory in iroh: every connection must declare which protocol it speaks, so an empty ALPN is rejected with ConnectWithOptsError::InvalidAlpn.

Solutions

  1. Pass a non-empty ALPN byte string, e.g. b"my-app/proto/1".
  2. Validate alpn.is_empty() at the call site and return a clearer application-level error.
  3. Set the ALPN in configuration or make it a required constructor parameter instead of a default empty value.
  4. Ensure the same ALPN is used by the accepting endpoint so the connection also succeeds post-handshake.

Example fix

// before
endpoint.connect(remote_id, b"").await?;
// after
endpoint.connect(remote_id, b"my-app/1").await?;
Defensive patterns

Strategy: validation

Validate before calling

fn valid_alpn(alpn: &[u8]) -> bool { !alpn.is_empty() }

Type guard

fn non_empty_alpn(alpn: &[u8]) -> Option<&[u8]> { (!alpn.is_empty()).then_some(alpn) }

Try / catch

match endpoint.connect(id, alpn).await {
    Err(ConnectWithOptsError::InvalidAlpn) => bail!("ALPN must be non-empty"),
    other => other?,
}

Prevention

When it happens

Trigger: Calling endpoint.connect(endpoint_id, b"") or passing an empty/zero-length alpn slice — often from a constant that was never set, or a protocol field deserialized as empty.

Common situations: Placeholder ALPN constants left as empty strings, config-driven ALPN values missing and defaulting to "", generic connect wrappers forwarding empty protocol fields from the caller.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of n0-computer/iroh@2b4de030ce (2026-09-08). Data as JSON: /api/errors/704152014bb6d41f. Report an issue: GitHub.

Appendix: source

Thrown at iroh/src/endpoint.rs:1123

    ) -> Result<Connecting, ConnectWithOptsError> {
        if self.is_closed() {
            return Err(e!(ConnectWithOptsError::EndpointClosed));
        }

        let endpoint_addr: EndpointAddr = endpoint_addr.into();
        let endpoint_id = endpoint_addr.id;

        Span::current().record("remote", tracing::field::display(endpoint_id.fmt_short()));

        if let BeforeConnectOutcome::Reject =
            self.inner.hooks.before_connect(&endpoint_addr, alpn).await
        {
            return Err(e!(ConnectWithOptsError::LocallyRejected));
        }

        // Connecting to ourselves is not supported.
        ensure!(endpoint_id != self.id(), ConnectWithOptsError::SelfConnect);
        ensure!(!alpn.is_empty(), ConnectWithOptsError::InvalidAlpn);

        event!(
            target: "iroh::_events::conn::connecting",
            tracing::Level::DEBUG,
            remote_id = %endpoint_id.fmt_short(),
            alpn = %String::from_utf8_lossy(alpn),
        );

        debug!(
            relay_url = ?endpoint_addr.relay_urls().next().cloned(),
            ip_addresses = ?endpoint_addr.ip_addrs().cloned().collect::<Vec<_>>(),
            "connecting",
        );

        let mapped_addr = self.inner.resolve_remote(endpoint_addr).await??;

        let transport_config = options
            .transport_config

View on GitHub (pinned to 2b4de030ce)