aboutsummaryrefslogtreecommitdiffstats
path: root/src/transport/libp2p/errors.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-31 04:20:41 +0100
committertv <tv@krebsco.de>2026-10-31 04:20:41 +0100
commit228eb88d89efb1edf875abbc3c8a8449a916061c (patch)
tree15697a50b435573664c441b1ea28c1ac9d9a9363 /src/transport/libp2p/errors.rs
parentc1653a2ac480bac397a535b0783996511abacb7d (diff)
libp2p: explode!
Diffstat (limited to 'src/transport/libp2p/errors.rs')
-rw-r--r--src/transport/libp2p/errors.rs128
1 files changed, 128 insertions, 0 deletions
diff --git a/src/transport/libp2p/errors.rs b/src/transport/libp2p/errors.rs
new file mode 100644
index 0000000..7eb4551
--- /dev/null
+++ b/src/transport/libp2p/errors.rs
@@ -0,0 +1,128 @@
+//! What libp2p's errors say, for the log and the figures: dial failures, relay
+//! reservations refused or lost.
+
+use super::*;
+
+/// A dial error, shortly: for each address tried (without the peer ID the caller names
+/// already), the innermost cause, once. libp2p's own text nests each transport's errors
+/// in the next ("Multiple dial errors occurred: ..."), repeating the cause at each level.
+pub(super) fn describe_dial_error(error: &::libp2p::swarm::DialError) -> String {
+ use ::libp2p::swarm::DialError;
+ let DialError::Transport(errors) = error else {
+ return error.to_string();
+ };
+ let described: Vec<String> = errors
+ .iter()
+ .map(|(address, error)| {
+ let mut address = address.clone();
+ if let Some(Protocol::P2p(_)) = address.iter().last() {
+ address.pop();
+ }
+ format!("{address}: {}", innermost_causes(error))
+ })
+ .collect();
+ if described.is_empty() {
+ return error.to_string();
+ }
+ described.join("; ")
+}
+
+/// Why a slot on a relay ended, or was never granted, shortly: libp2p nests the
+/// relay's answer several types deep (`Left(Left(Left(Reservation(...))))`), and the
+/// common cases deserve a sentence.
+pub(super) fn relay_failure(reason: &std::io::Result<()>) -> String {
+ let Err(error) = reason else {
+ return "closed".to_owned();
+ };
+ relay_failure_text(&format!("{error:?}"), &innermost_causes(error))
+}
+
+/// `relay_failure` on the error's debug form, and its innermost cause as text.
+pub(super) fn relay_failure_text(debug: &str, innermost: &str) -> String {
+ let known = [
+ (
+ "NoAddressesInReservation",
+ "the relay gave no address to be reached through it",
+ ),
+ ("ResourceLimitExceeded", "the relay has no slot left"),
+ ("PermissionDenied", "the relay doesn't let us reserve"),
+ ("NoReservation", "the relay holds no slot for us"),
+ (
+ "connection gated",
+ "the relay refused the connection (connection gated)",
+ ),
+ (
+ "UnsupportedProtocol",
+ "the relay doesn't relay (no circuit relay protocol)",
+ ),
+ ("Timeout", "the relay didn't answer in time"),
+ ];
+ if let Some((_, said)) = known.iter().find(|(pattern, _)| debug.contains(pattern)) {
+ return (*said).to_owned();
+ }
+ // The protocol refused, but only after its first data (`swarm_config`).
+ if innermost == "Failed" {
+ return "the relay refused the circuit relay protocol (is it a relay?)".to_owned();
+ }
+ innermost.to_owned()
+}
+
+/// Whether a slot ended, or wasn't granted, because the relay refused it (its error's
+/// debug form and innermost cause, as for `relay_failure_text`), rather than because it
+/// didn't answer or the connection closed: only then is it worth not asking again for a
+/// while.
+pub(super) fn relay_refused(debug: &str, innermost: &str) -> bool {
+ let refusals = [
+ "NoAddressesInReservation",
+ "ResourceLimitExceeded",
+ "PermissionDenied",
+ "connection gated",
+ "UnsupportedProtocol",
+ "WrongPeerId",
+ ];
+ refusals.iter().any(|pattern| debug.contains(pattern)) || innermost == "Failed"
+}
+
+/// The innermost of `error`'s causes, each distinct one once: those libp2p lists in a
+/// "Multiple dial errors occurred:" text (one per line, ` - <cause>: <its cause>...`),
+/// or the last of the source chain.
+pub(super) fn innermost_causes(error: &(dyn std::error::Error + 'static)) -> String {
+ let mut innermost = error;
+ while let Some(source) = innermost.source() {
+ innermost = source;
+ }
+ let text = innermost.to_string();
+ let mut causes: Vec<&str> = Vec::new();
+ for line in text.lines() {
+ let Some(listed) = line.trim().strip_prefix("- ") else {
+ continue;
+ };
+ // A line ends in the next level's heading when that level lists more.
+ let cause = listed
+ .split(": ")
+ .filter(|part| !part.starts_with("Multiple dial errors"))
+ .last()
+ .unwrap_or(listed)
+ .trim();
+ if !cause.is_empty() && !causes.contains(&cause) {
+ causes.push(cause);
+ }
+ }
+ if causes.is_empty() {
+ let cause = text.rsplit(": ").next().unwrap_or(&text).trim();
+ return cause.to_owned();
+ }
+ causes.join(", ")
+}
+
+/// Counts a dial that failed with `error` (not one that wasn't needed).
+pub(super) fn count_dial_failure(failures: &mut DialFailures, error: &::libp2p::swarm::DialError) {
+ use ::libp2p::swarm::DialError;
+ match error {
+ DialError::DialPeerConditionFalse(_) => {}
+ DialError::Transport(_) => failures.unreachable += 1,
+ DialError::WrongPeerId { .. } => failures.wrong_peer_id += 1,
+ DialError::NoAddresses => failures.no_addresses += 1,
+ _ => failures.other += 1,
+ }
+}