mfontanini/presenterm · error · anyhow::Error

failed to create speaker notes publisher: {e}

Error message

failed to create speaker notes publisher: {e}

What it means

This error is raised in `SpeakerNotesComponents::new` when `SpeakerNotesEventPublisher::new` returns an `io::Error`. The publisher constructor binds a UDP socket to 127.0.0.1:0, enables broadcast, and connects it to the configured publish address; any of those OS-level socket operations failing is wrapped in this anyhow context. It means presenterm could not set up the speaker-notes UDP publisher and the presentation cannot start with publishing enabled.

Solutions

  1. Check the `speaker_notes.publish_address` in your config (or the relevant CLI flag) is a valid IPv4 SocketAddr, e.g. the default 127.0.0.1 style address with a free port.
  2. Run `ulimit -n` and raise the file-descriptor limit if bind fails with 'Too many open files'.
  3. If running in a container/sandbox, allow loopback UDP socket creation (don't use network isolation that strips loopback).
  4. Use the raw error after the colon (the io::Error message) to identify whether bind, set_broadcast, or connect failed, and address that specific cause.

Example fix

// before (typo'd / invalid publish address in config.toml)
[speaker_notes]
publish_address = "localhost:48700"  // or an IPv6 address

// after
[speaker_notes]
publish_address = "127.0.0.1:48700"
Defensive patterns

Strategy: validation

Validate before calling

// Before launching with --publish-speaker-notes, verify the publish address is bindable/reachable:
use std::net::{UdpSocket, SocketAddr};
fn validate_publish_address(addr: SocketAddr) -> Result<(), String> {
    let s = UdpSocket::bind("127.0.0.1:0")
        .map_err(|e| format!("cannot bind ephemeral port: {e}"))?;
    s.connect(addr)
        .map_err(|e| format!("cannot connect to publish address {addr}: {e}"))
}

Try / catch

// In Rust the error is already context-wrapped; match on the io cause:
match SpeakerNotesComponents::new(...) {
    Ok(components) => components,
    Err(e) if e.to_string().contains("speaker notes publisher") => {
        eprintln!("publisher setup failed: {e:#}; check speaker_notes.publish_address");
        std::process::exit(1);
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: The CLI was started with `--publish-speaker-notes` (or the config has `speaker_notes.always_publish` true and `--listen-speaker-notes` is not set), and `SpeakerNotesEventPublisher::new(config.speaker_notes.publish_address, ...)` fails: the UDP bind to 127.0.0.1:0 fails (no ephemeral ports available, socket resource limits), `set_broadcast` is rejected, or `connect` to the publish address fails (invalid/unroutable address family, e.g. an IPv6 publish address against the IPv4-bound socket, or network-unreachable).

Common situations: Misconfigured `speaker_notes.publish_address` in the config (wrong port, IPv6 address, or malformed SocketAddr); running in a sandbox/container that blocks socket creation (seccomp, no network namespace); hitting the OS file-descriptor limit so bind fails with EMFILE; running on a host without a loopback interface.

Understand the failure class

Background: ECONNREFUSED and "connection refused" / "could not connect to server" errors: what they mean and how to fix them — this error's family across 44 libraries.

Related errors


AI-assisted analysis of mfontanini/presenterm@5f8add11a2 (2026-09-12). Data as JSON: /api/errors/fe24f31a03c8c0b7. Report an issue: GitHub.

Appendix: source

Thrown at src/main.rs:372

    }
}

struct SpeakerNotesComponents {
    events_listener: Option<SpeakerNotesEventListener>,
    events_publisher: Option<SpeakerNotesEventPublisher>,
}

impl SpeakerNotesComponents {
    fn new(cli: &Cli, config: &Config, path: &Path) -> anyhow::Result<Self> {
        let full_presentation_path = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
        let publish_speaker_notes =
            cli.publish_speaker_notes || (config.speaker_notes.always_publish && !cli.listen_speaker_notes);
        let events_publisher = publish_speaker_notes
            .then(|| {
                SpeakerNotesEventPublisher::new(config.speaker_notes.publish_address, full_presentation_path.clone())
            })
            .transpose()
            .map_err(|e| anyhow!("failed to create speaker notes publisher: {e}"))?;
        let events_listener = cli
            .listen_speaker_notes
            .then(|| SpeakerNotesEventListener::new(config.speaker_notes.listen_address, full_presentation_path))
            .transpose()
            .map_err(|e| anyhow!("failed to create speaker notes listener: {e}"))?;
        Ok(Self { events_listener, events_publisher })
    }
}

fn overflow_validation_enabled(mode: &PresentMode, config: &ValidateOverflows) -> bool {
    match (config, mode) {
        (ValidateOverflows::Always, _) => true,
        (ValidateOverflows::Never, _) => false,
        (ValidateOverflows::WhenPresenting, PresentMode::Presentation) => true,
        (ValidateOverflows::WhenDeveloping, PresentMode::Development) => true,
        _ => false,
    }
}

View on GitHub (pinned to 5f8add11a2)