stamparm/maltrail · error
bind
Error message
bind
What it means
The metrics test harness spawn() binds a TcpListener on 127.0.0.1:0 (an OS-assigned port) and .expect("bind") panics if binding fails. With port 0, failure usually means the loopback interface is unavailable, resource limits are exhausted, or the network stack is restricted in the test environment.
Solutions
- Retry the spawn once after a short delay if the failure is transient resource exhaustion
- Verify loopback networking is available in the test environment (ulimit -n, container --network settings)
- Surface the io::Error from bind instead of a bare expect for diagnosis
- Raise the file-descriptor limit (ulimit -n) if sockets are being leaked by other tests
Example fix
// before
let bound = spawn("127.0.0.1:0", registry, std::time::Instant::now()).expect("bind");
// after
let bound = spawn("127.0.0.1:0", registry, std::time::Instant::now())
.unwrap_or_else(|e| panic!("metrics listener bind failed: {e}")); Defensive patterns
Strategy: retry
Validate before calling
std::net::TcpListener::bind("127.0.0.1:0").map(|l| drop(l)).map_err(|e| format!("loopback bind unavailable: {e}"))?; Try / catch
let bound = spawn("127.0.0.1:0", registry, now).unwrap_or_else(|e| panic!("bind failed: {e}")); Prevention
- Ensure CI containers allow loopback networking
- Raise ulimit -n in test environments
- Always bind to port 0 and reuse the returned address
When it happens
Trigger: Calling spawn("127.0.0.1:0", registry, now) in the test a_scrape_returns_the_metrics_over_http when TcpListener::bind returns Err — e.g. no loopback, file-descriptor/socket exhaustion, or sandboxed CI without network permissions.
Common situations: CI containers with networking disabled; hitting the open-file limit after many tests leak listeners; hardened sandboxes (seccomp) blocking socket creation.
Related errors
AI-assisted analysis of stamparm/maltrail@77cfb06d76 (2026-09-13).
Data as JSON: /api/errors/7ca61b2380914ad8.
Report an issue: GitHub.
Appendix: source
Thrown at sensor/src/stats.rs:336
"maltrail_worker_packets_total",
] {
assert!(text.contains(&format!("# HELP {name} ")), "missing HELP for {name}:\n{text}");
assert!(text.contains(&format!("# TYPE {name} ")), "missing TYPE for {name}");
}
assert!(text.contains("maltrail_trails 1505265"), "{text}");
assert!(text.contains("maltrail_workers 2"), "{text}");
assert!(text.contains("maltrail_worker_packets_total{worker=\"1\"}"), "{text}");
// Monotonic counters must end in _total so `rate()` works.
for line in text.lines().filter(|l| l.starts_with("# TYPE ") && l.ends_with(" counter")) {
let name = line.split_whitespace().nth(2).unwrap();
assert!(name.ends_with("_total"), "counter {name} must end in _total");
}
}
#[test]
fn a_scrape_returns_the_metrics_over_http() {
let registry = Arc::new(Registry::new(1));
let bound = spawn("127.0.0.1:0", registry, std::time::Instant::now()).expect("bind");
let mut stream = std::net::TcpStream::connect(bound).expect("connect");
stream.write_all(b"GET /metrics HTTP/1.1\r\nHost: localhost\r\n\r\n").unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).unwrap();
assert!(response.starts_with("HTTP/1.1 200 OK"), "{response}");
assert!(response.contains("text/plain; version=0.0.4"), "{response}");
assert!(response.contains("maltrail_up 1"), "{response}");
}
#[test]
fn a_bad_address_is_reported_not_fatal() {
let registry = Arc::new(Registry::new(1));
assert!(spawn("300.300.300.300:1", registry, std::time::Instant::now()).is_err());
}
}
View on GitHub (pinned to 77cfb06d76)