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
- 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.
- Run `ulimit -n` and raise the file-descriptor limit if bind fails with 'Too many open files'.
- If running in a container/sandbox, allow loopback UDP socket creation (don't use network isolation that strips loopback).
- 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
- Keep publish_address an IPv4 loopback SocketAddr (e.g. 127.0.0.1:<port>) as the defaults intend.
- Raise the nofile ulimit in environments that open many sockets.
- Test publisher/listener setup in containers and sandboxes before presenting.
- Don't disable loopback networking where presenterm runs.
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)