stamparm/maltrail · error

connect

Error message

connect

What it means

The test connects a TcpStream to the listener returned by spawn() and .expect("connect") panics if the connection cannot be established. Since the address is a just-bound loopback listener, failure indicates the server thread died, the listener was closed, or the connection was refused/timed out in the environment.

Solutions

  1. Check whether the spawned server thread panicked (capture its JoinHandle/panic output) before connecting
  2. Add a readiness wait (poll until the port accepts) instead of connecting immediately after spawn
  3. Verify loopback connections are permitted in the CI/sandbox environment
  4. Surface the io::Error in the panic message for diagnosis

Example fix

// before
let mut stream = std::net::TcpStream::connect(bound).expect("connect");
// after
let mut stream = std::net::TcpStream::connect(bound)
    .unwrap_or_else(|e| panic!("connect to metrics server {bound} failed: {e}"));
Defensive patterns

Strategy: retry

Validate before calling

std::net::TcpStream::connect_timeout(&bound, std::time::Duration::from_secs(2)).map(|s| drop(s)).map_err(|e| format!("metrics server not accepting: {e}"))?;

Try / catch

let mut stream = std::net::TcpStream::connect(bound).unwrap_or_else(|e| panic!("connect to {bound}: {e}"));

Prevention

When it happens

Trigger: TcpStream::connect(bound) in a_scrape_returns_the_metrics_over_http returns Err because the spawned metrics server thread exited or never accepted, the listener was dropped, or loopback connections are blocked.

Common situations: Server thread panicking before accept; spawn() returning an address whose listener was immediately dropped; sandboxed CI blocking loopback connect; firewall or seccomp policy refusing local sockets.

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 stamparm/maltrail@77cfb06d76 (2026-09-13). Data as JSON: /api/errors/0319294ea7cedca0. Report an issue: GitHub.

Appendix: source

Thrown at sensor/src/stats.rs:337

        ] {
            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)