stalwartlabs/stalwart · warning · io::Error(InvalidData)
Invalid line: {raw_response}
Error message
Invalid line: {raw_response} What it means
Raised while parsing a Pyzor UDP response when the value of the 'code' field cannot be parsed as a u32. The parser in pyzor_send_message splits each line on ':' and wraps parse failures in an InvalidData io::Error that embeds the entire raw response. It indicates the peer did not answer with a well-formed Pyzor key:value reply.
Source
Thrown at crates/spam-filter/src/modules/pyzor.rs:108
let socket = UdpSocket::bind("0.0.0.0:0").await?;
tokio::time::timeout(timeout, socket.send_to(message.as_bytes(), addr)).await??;
let mut buffer = vec![0u8; 1024];
let (size, _) = tokio::time::timeout(timeout, socket.recv_from(&mut buffer)).await??;
let raw_response = std::str::from_utf8(&buffer[..size])
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
let mut response = PyzorResponse {
code: u32::MAX,
count: u64::MAX,
wl_count: u64::MAX,
};
for line in raw_response.lines() {
if let Some((k, v)) = line.split_once(':') {
if k.eq_ignore_ascii_case("code") {
response.code = v.trim().parse().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid line: {raw_response}"),
)
})?;
} else if k.eq_ignore_ascii_case("count") {
response.count = v.trim().parse().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid line: {raw_response}"),
)
})?;
} else if k.eq_ignore_ascii_case("wl-count") {
response.wl_count = v.trim().parse().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid line: {raw_response}"),
)
})?;View on GitHub (pinned to e962003857)
Solutions
- Verify the configured Pyzor server address (config.address) is a genuine, reachable Pyzor server
- Probe the server manually (pyzor check) and inspect the raw reply it returns
- Log the raw_response embedded in this error's message to see the malformed payload
- Check for proxy/captive-portal interference with UDP traffic
- Switch to a healthy public Pyzor mirror or upgrade the server
Defensive patterns
Strategy: validation
Validate before calling
// validate a pyzor reply before trusting it
fn is_valid_pyzor_response(raw: &str) -> bool {
raw.lines().any(|l| {
l.split_once(':')
.map(|(k, v)| k.eq_ignore_ascii_case("code") && v.trim().parse::<u32>().is_ok())
.unwrap_or(false)
})
} Try / catch
match pyzor_check(&config, &message).await {
Ok(score) => score,
Err(e) if e.is::<trc::SpamEvent>() && e.reason_is_parse_failure() => {
tracing::warn!("pyzor reply unparseable; skipping pyzor score");
Default::default()
}
Err(e) => return Err(e),
} Prevention
- Confirm the Pyzor server address/port resolves to a genuine Pyzor server before enabling the module
- Health-check the Pyzor server periodically and disable scoring when replies are malformed
- Log raw responses (already embedded in the error) to spot proxies or error pages
- Keep the server address out of hostile/intercepted networks (no captive portals)
When it happens
Trigger: pyzor_send_message receives a UDP datagram containing a 'code:' line whose value is not a valid u32 (empty value, non-numeric text, negative number, or an HTML/human-readable error message). Called via pyzor_check.
Common situations: config.address pointing at a wrong host/port where a different service answers; Pyzor server returning an error page instead of protocol fields; captive portal or proxy intercepting UDP traffic; corrupted or truncated datagrams.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
AI-assisted analysis of stalwartlabs/stalwart@e962003857 (2026-09-06).
Data as JSON: /api/errors/6818fd65086496c1.
Report an issue: GitHub.