fix(rust): introduce dedicated downcast functions for anyhow (#10966)

The downcasting abilities of `anyhow` are pretty powerful.
Unfortunately, they can also be a bit tricky to get right. Whilst `is`
and `downcast` work fine for any errors that are within the `anyhow`
error chain, they don't check the chain of errors prior to that. In
other words, if we already have a nested `std::error::Error` with
several causes, `anyhow` cannot downcast to these causes directly.

In order to avoid this footgun, we create a thin-layer on top of the
`anyhow` crate with some downcasting functions that always try to do the
right thing.
This commit is contained in:
Thomas Eizinger
2025-11-25 15:14:17 +11:00
committed by GitHub
parent 48e0a89125
commit bcf4ccf817
20 changed files with 249 additions and 80 deletions

58
rust/Cargo.lock generated
View File

@@ -311,6 +311,14 @@ version = "1.0.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100"
[[package]]
name = "anyhow-ext"
version = "0.1.0"
dependencies = [
"anyhow",
"thiserror 2.0.17",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
@@ -1293,7 +1301,7 @@ name = "client-ffi"
version = "0.1.0"
dependencies = [
"android_log-sys",
"anyhow",
"anyhow-ext",
"backoff",
"client-shared",
"connlib-model",
@@ -1327,7 +1335,7 @@ dependencies = [
name = "client-shared"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"backoff",
"bimap",
"chrono",
@@ -2043,7 +2051,7 @@ dependencies = [
name = "dns-over-tcp"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"dns-types",
"firezone-bin-shared",
"firezone-logging",
@@ -2352,7 +2360,7 @@ dependencies = [
name = "firezone-bin-shared"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"atomicwrites",
"axum",
"bufferpool",
@@ -2405,7 +2413,7 @@ dependencies = [
name = "firezone-cli"
version = "1.0.0"
dependencies = [
"anyhow",
"anyhow-ext",
"clap",
"nix 0.30.1",
"rpassword",
@@ -2418,7 +2426,7 @@ dependencies = [
name = "firezone-gateway"
version = "1.4.19"
dependencies = [
"anyhow",
"anyhow-ext",
"backoff",
"boringtun",
"caps",
@@ -2468,7 +2476,7 @@ name = "firezone-gui-client"
version = "1.5.9"
dependencies = [
"admx-macro",
"anyhow",
"anyhow-ext",
"arboard",
"atomicwrites",
"backoff",
@@ -2536,7 +2544,7 @@ dependencies = [
name = "firezone-headless-client"
version = "1.5.5"
dependencies = [
"anyhow",
"anyhow-ext",
"backoff",
"clap",
"client-shared",
@@ -2571,7 +2579,7 @@ dependencies = [
name = "firezone-logging"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"firezone-telemetry",
"nu-ansi-term",
"output_vt100",
@@ -2591,7 +2599,7 @@ dependencies = [
name = "firezone-relay"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"axum",
"aya",
"aya-build",
@@ -2643,7 +2651,7 @@ dependencies = [
name = "firezone-telemetry"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"flume",
"futures",
"hex",
@@ -2668,7 +2676,7 @@ dependencies = [
name = "firezone-tunnel"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"base64 0.22.1",
"bimap",
"boringtun",
@@ -3293,7 +3301,7 @@ dependencies = [
name = "gui-smoke-test"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"clap",
"subprocess",
"tracing",
@@ -3554,7 +3562,7 @@ dependencies = [
name = "http-client"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"bytes",
"futures",
"http 1.3.1",
@@ -3576,7 +3584,7 @@ dependencies = [
name = "http-test-server"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"axum",
"futures",
"serde",
@@ -3908,7 +3916,7 @@ dependencies = [
name = "ip-packet"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"arbitrary",
"bufferpool",
"etherparse",
@@ -4164,7 +4172,7 @@ dependencies = [
name = "l3-tcp"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"ip-packet",
"smoltcp",
"tracing",
@@ -4174,7 +4182,7 @@ dependencies = [
name = "l3-udp-dns-client"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"dns-types",
"ip-packet",
"rand 0.8.5",
@@ -4185,7 +4193,7 @@ dependencies = [
name = "l4-tcp-dns-server"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"dns-types",
"futures",
"tokio",
@@ -4196,7 +4204,7 @@ dependencies = [
name = "l4-udp-dns-client"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"dns-types",
"futures",
"socket-factory",
@@ -4208,7 +4216,7 @@ dependencies = [
name = "l4-udp-dns-server"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"dns-types",
"futures",
"tokio",
@@ -5492,7 +5500,7 @@ dependencies = [
name = "phoenix-channel"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"backoff",
"base64 0.22.1",
"firezone-logging",
@@ -7072,7 +7080,7 @@ dependencies = [
name = "snownet"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"bnum",
"boringtun",
"bufferpool",
@@ -7099,7 +7107,7 @@ dependencies = [
name = "socket-factory"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"bufferpool",
"bytes",
"derive_more 2.0.1",
@@ -8533,7 +8541,7 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
name = "tun"
version = "0.1.0"
dependencies = [
"anyhow",
"anyhow-ext",
"ip-packet",
"libc",
"tokio",

View File

@@ -1,5 +1,6 @@
[workspace]
members = [
"anyhow-ext",
"bin-shared",
"cli",
"client-ffi",
@@ -44,7 +45,7 @@ edition = "2024"
[workspace.dependencies]
admx-macro = { path = "gui-client/src-admx-macro" }
anyhow = "1.0.99"
anyhow = { package = "anyhow-ext", path = "anyhow-ext" }
arbitrary = "1.4.2"
arboard = { version = "3.6.1", default-features = false }
async-trait = { version = "0.1", default-features = false }

View File

@@ -0,0 +1,14 @@
[package]
name = "anyhow-ext"
version = "0.1.0"
edition = { workspace = true }
license = { workspace = true }
[lib]
path = "lib.rs"
[dependencies]
anyhow = "1.0.99"
[dev-dependencies]
thiserror = { workspace = true }

152
rust/anyhow-ext/lib.rs Normal file
View File

@@ -0,0 +1,152 @@
pub use anyhow::*;
pub trait ErrorExt {
fn any_is<T>(&self) -> bool
where
T: std::error::Error + Send + Sync + 'static;
fn any_downcast_ref<T>(&self) -> Option<&T>
where
T: std::error::Error + Send + Sync + 'static;
}
impl ErrorExt for anyhow::Error {
#[expect(
clippy::disallowed_methods,
reason = "We are implementing the alternative."
)]
fn any_is<T>(&self) -> bool
where
T: std::error::Error + Send + Sync + 'static,
{
self.is::<T>() || self.chain().any(|e| e.is::<T>())
}
#[expect(
clippy::disallowed_methods,
reason = "We are implementing the alternative."
)]
fn any_downcast_ref<T>(&self) -> Option<&T>
where
T: std::error::Error + Send + Sync + 'static,
{
std::iter::empty()
.chain(self.downcast_ref::<T>())
.chain(self.chain().flat_map(|e| e.downcast_ref::<T>()))
.next()
}
}
#[cfg(test)]
mod tests {
use std::io;
use anyhow::{Context, Result};
use super::*;
#[test]
fn any_is_works_for_context() {
let error = Result::<(), _>::Err(io::Error::other("Test"))
.context("Foobar")
.unwrap_err();
assert!(error.any_is::<io::Error>())
}
#[test]
fn any_is_works_for_typed_context() {
let error = Result::<(), _>::Err(io::Error::other("Test"))
.context(FooError)
.unwrap_err();
assert!(error.any_is::<FooError>())
}
#[test]
fn any_is_works_for_anyhow_new() {
let error = Result::<(), _>::Err(anyhow::Error::new(io::Error::other("Test")))
.context("Foobar")
.unwrap_err();
assert!(error.any_is::<io::Error>())
}
#[test]
fn any_is_works_for_custom_error_with_source() {
let error = Result::<(), _>::Err(BazError(BarError(FooError)))
.context("Foobar")
.unwrap_err();
assert!(error.any_is::<BazError>());
assert!(error.any_is::<BarError>());
assert!(error.any_is::<FooError>());
}
#[test]
fn any_downcast_ref_works_for_context() {
let error = Result::<(), _>::Err(io::Error::other("Test"))
.context("Foobar")
.unwrap_err();
assert_eq!(
error.any_downcast_ref::<io::Error>().unwrap().to_string(),
"Test"
)
}
#[test]
fn any_downcast_ref_works_for_typed() {
let error = Result::<(), _>::Err(io::Error::other("Test"))
.context(FooError)
.unwrap_err();
assert_eq!(
error.any_downcast_ref::<FooError>().unwrap().to_string(),
"Foo"
)
}
#[test]
fn any_downcast_ref_works_for_anyhow_new() {
let error = Result::<(), _>::Err(anyhow::Error::new(io::Error::other("Test")))
.context("Foobar")
.unwrap_err();
assert_eq!(
error.any_downcast_ref::<io::Error>().unwrap().to_string(),
"Test"
)
}
#[test]
fn any_downcast_ref_works_for_custom_error_with_source() {
let error = Result::<(), _>::Err(BazError(BarError(FooError)))
.context("Foobar")
.unwrap_err();
assert_eq!(
error.any_downcast_ref::<BazError>().unwrap().to_string(),
"Baz"
);
assert_eq!(
error.any_downcast_ref::<BarError>().unwrap().to_string(),
"Bar"
);
assert_eq!(
error.any_downcast_ref::<FooError>().unwrap().to_string(),
"Foo"
);
}
#[derive(Debug, thiserror::Error)]
#[error("Foo")]
struct FooError;
#[derive(Debug, thiserror::Error)]
#[error("Bar")]
struct BarError(#[from] FooError);
#[derive(Debug, thiserror::Error)]
#[error("Baz")]
struct BazError(#[from] BarError);
}

View File

@@ -1,5 +1,5 @@
use crate::PHOENIX_TOPIC;
use anyhow::{Context as _, Result};
use anyhow::{Context as _, ErrorExt as _, Result};
use connlib_model::{PublicKey, ResourceView};
use firezone_tunnel::messages::RelaysPresence;
use firezone_tunnel::messages::client::{
@@ -88,7 +88,7 @@ impl From<anyhow::Error> for DisconnectError {
impl DisconnectError {
pub fn is_authentication_error(&self) -> bool {
let Some(e) = self.0.downcast_ref::<phoenix_channel::Error>() else {
let Some(e) = self.0.any_downcast_ref::<phoenix_channel::Error>() else {
return false;
};
@@ -308,8 +308,7 @@ impl Eventloop {
fn handle_tunnel_error(&mut self, mut e: TunnelError) -> Result<()> {
for e in e.drain() {
if e.root_cause()
.downcast_ref::<io::Error>()
if e.any_downcast_ref::<io::Error>()
.is_some_and(is_unreachable)
{
tracing::debug!("{e:#}"); // Log these on DEBUG so they don't go completely unnoticed.
@@ -317,16 +316,14 @@ impl Eventloop {
}
// Invalid Input can be all sorts of things but we mostly see it with unreachable addresses.
if e.root_cause()
.downcast_ref::<io::Error>()
if e.any_downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::InvalidInput)
{
tracing::debug!("{e:#}");
continue;
}
if e.root_cause()
.downcast_ref::<io::Error>()
if e.any_downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::PermissionDenied)
{
if !mem::replace(&mut self.logged_permission_denied, true) {
@@ -338,9 +335,7 @@ impl Eventloop {
continue;
}
if e.root_cause()
.is::<firezone_tunnel::UdpSocketThreadStopped>()
{
if e.any_is::<firezone_tunnel::UdpSocketThreadStopped>() {
return Err(e);
}

View File

@@ -6,4 +6,9 @@ disallowed-methods = [
{ path = "std::collections::HashSet::iter", reason = "HashSet has non-deterministic iteration order, use BTreeSet instead" },
{ path = "std::collections::HashMap::iter_mut", reason = "HashMap has non-deterministic iteration order, use BTreeMap instead" },
{ path = "tracing::subscriber::set_global_default", reason = "Does not init `LogTracer`, use `firezone_logging::init` instead." },
{ path = "anyhow::Error::is", reason = "Does not check entire source chain, use `any_is` instead" },
{ path = "anyhow::Error::downcast_ref", reason = "Does not check entire source chain, use `any_downcast_ref` instead" },
{ path = "anyhow::Error::downcast_mut", reason = "Does not check entire source chain, use `any_downcast_ref` instead" },
{ path = "anyhow::Error::downcast", reason = "Does not check entire source chain, use `any_downcast_ref` instead" },
{ path = "anyhow::Error::root_cause", reason = "Does not check entire source chain, use `any_downcast_ref` instead" },
]

View File

@@ -484,6 +484,7 @@ mod tests {
(id, idx, key)
}
#[expect(clippy::disallowed_methods, reason = "This is a test.")]
fn assert_disconnected(
connections: &mut Connections<u32, u32>,
id: u32,

View File

@@ -518,8 +518,9 @@ fn is_equal_modulo_scope_for_ipv6_link_local(expected: SocketAddr, actual: Socke
#[cfg(any(target_os = "linux", target_os = "android"))]
fn is_os_error_5(e: &anyhow::Error) -> bool {
e.root_cause()
.downcast_ref::<io::Error>()
use anyhow::ErrorExt;
e.any_downcast_ref::<io::Error>()
.and_then(|e| e.raw_os_error())
.is_some_and(|c| c == libc::EIO)
}

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, ErrorExt, Result, bail};
use ip_packet::{IpPacket, IpPacketBuf};
use std::io;
use std::os::fd::AsRawFd;
@@ -80,7 +80,7 @@ where
break;
};
}
Err(e) if e.root_cause().is::<ip_packet::Fragmented>() => {
Err(e) if e.any_is::<ip_packet::Fragmented>() => {
tracing::debug!("{e:#}"); // Log on debug to be less noisy.
continue;
}
@@ -109,6 +109,6 @@ mod tests {
let final_error = anyhow::Error::new(io_error).context("Failed to read from TUN fd");
assert!(final_error.root_cause().is::<ip_packet::Fragmented>())
assert!(final_error.any_is::<ip_packet::Fragmented>())
}
}

View File

@@ -26,7 +26,7 @@ use crate::messages::{IceCredentials, SecretKey};
use crate::peer_store::PeerStore;
use crate::unique_packet_buffer::UniquePacketBuffer;
use crate::{IPV4_TUNNEL, IPV6_TUNNEL, IpConfig, TunConfig, dns, is_peer, p2p_control};
use anyhow::Context;
use anyhow::{Context, ErrorExt};
use connlib_model::{
GatewayId, IceCandidate, PublicKey, RelayId, ResourceId, ResourceStatus, ResourceView,
};
@@ -520,7 +520,7 @@ impl ClientState {
}
Err(e)
if response.transport == dns::Transport::Udp
&& e.downcast_ref::<io::Error>()
&& e.any_downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::TimedOut) =>
{
tracing::debug!("Recursive UDP DNS query timed out");

View File

@@ -15,7 +15,7 @@ use crate::messages::gateway::{Client, ResourceDescription, Subject};
use crate::messages::{Answer, IceCredentials, ResolveRequest};
use crate::peer_store::PeerStore;
use crate::{FailedToDecapsulate, GatewayEvent, IpConfig, p2p_control, packet_kind};
use anyhow::{Context, Result};
use anyhow::{Context, ErrorExt, Result};
use boringtun::x25519::{self, PublicKey};
use chrono::{DateTime, Utc};
use connlib_model::{ClientId, IceCandidate, RelayId, ResourceId};
@@ -133,7 +133,7 @@ impl GatewayState {
let encrypted_packet = match self.node.encapsulate(cid, &packet, now) {
Ok(Some(encrypted_packet)) => encrypted_packet,
Ok(None) => return Ok(None),
Err(e) if e.is::<snownet::UnknownConnection>() => {
Err(e) if e.any_is::<snownet::UnknownConnection>() => {
return Err(e.context(UnroutablePacket::not_connected(&packet)));
}
Err(e) => return Err(e),

View File

@@ -1024,6 +1024,7 @@ mod tests {
now += nat_table::UDP_TTL;
peer.handle_timeout(now);
#[expect(clippy::disallowed_methods, reason = "This is a test.")]
let err = peer
.translate_inbound(response, now)
.unwrap_err()

View File

@@ -5,7 +5,7 @@ mod tcp_dns;
mod udp_dns;
use crate::{TunnelError, device_channel::Device, dns, otel, sockets::Sockets};
use anyhow::{Context as _, Result};
use anyhow::{Context as _, ErrorExt, Result};
use chrono::{DateTime, Utc};
use dns_types::DoHUrl;
use futures::FutureExt as _;
@@ -361,7 +361,7 @@ impl Io {
if let Poll::Ready(response) = &dns_response
&& let dns::Upstream::DoH { server } = &response.server
&& let Err(e) = &response.message
&& e.is::<http_client::Closed>()
&& e.any_is::<http_client::Closed>()
{
tracing::debug!(%server, "Connection of DoH client failed");

View File

@@ -5,7 +5,7 @@
#![cfg_attr(test, allow(clippy::unwrap_used))]
use anyhow::{Context as _, Result};
use anyhow::{Context as _, ErrorExt as _, Result};
use chrono::Utc;
use connlib_model::{ClientId, GatewayId, IceCandidate, PublicKey, ResourceId, ResourceView};
use dns_types::DomainName;
@@ -448,7 +448,7 @@ impl GatewayTunnel {
}
Err(e) => {
let routing_error = e
.downcast_ref::<gateway::UnroutablePacket>()
.any_downcast_ref::<gateway::UnroutablePacket>()
.map(|e| e.reason())
.unwrap_or(gateway::RoutingError::Other);

View File

@@ -1,5 +1,5 @@
use crate::otel;
use anyhow::{Context as _, Result};
use anyhow::{Context as _, ErrorExt as _, Result};
use futures::{SinkExt, ready};
use gat_lending_iterator::LendingIterator;
use socket_factory::DatagramOut;
@@ -271,7 +271,7 @@ impl ThreadedUdpSocket {
tokio::task::yield_now().await;
if let Err(e) = socket.send(datagram).await {
if let Some(io) = e.downcast_ref::<io::Error>() {
if let Some(io) = e.any_downcast_ref::<io::Error>() {
io_error_counter.add(
1,
&[
@@ -307,7 +307,7 @@ impl ThreadedUdpSocket {
if let Some(io) = result
.as_ref()
.err()
.and_then(|e| e.downcast_ref::<io::Error>())
.and_then(|e| e.any_downcast_ref::<io::Error>())
{
io_error_counter.add(
1,

View File

@@ -1,4 +1,4 @@
use anyhow::{Context as _, Result};
use anyhow::{Context as _, ErrorExt, Result};
use boringtun::x25519::PublicKey;
#[cfg(not(target_os = "windows"))]
use dns_lookup::{AddrInfoHints, AddrInfoIter, LookupError};
@@ -288,8 +288,7 @@ impl Eventloop {
fn handle_tunnel_error(&mut self, mut e: TunnelError) -> Result<()> {
for e in e.drain() {
if e.root_cause()
.downcast_ref::<io::Error>()
if e.any_downcast_ref::<io::Error>()
.is_some_and(is_unreachable)
{
tracing::debug!("{e:#}"); // Log these on DEBUG so they don't go completely unnoticed.
@@ -297,16 +296,14 @@ impl Eventloop {
}
// Invalid Input can be all sorts of things but we mostly see it with unreachable addresses.
if e.root_cause()
.downcast_ref::<io::Error>()
if e.any_downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::InvalidInput)
{
tracing::debug!("{e:#}");
continue;
}
if e.root_cause()
.downcast_ref::<io::Error>()
if e.any_downcast_ref::<io::Error>()
.is_some_and(|e| e.kind() == io::ErrorKind::PermissionDenied)
{
if !mem::replace(&mut self.logged_permission_denied, true) {
@@ -318,20 +315,18 @@ impl Eventloop {
continue;
}
if e.root_cause().is::<ip_packet::ImpossibleTranslation>() {
if e.any_is::<ip_packet::ImpossibleTranslation>() {
// Some IP packets cannot be translated and should be dropped "silently".
// Do so by ignoring the error here.
continue;
}
if let Some(e) = e.downcast_ref::<firezone_tunnel::UnroutablePacket>() {
if let Some(e) = e.any_downcast_ref::<firezone_tunnel::UnroutablePacket>() {
tracing::debug!(src = %e.source(), dst = %e.destination(), proto = %e.proto(), "{e:#}");
continue;
}
if e.root_cause()
.is::<firezone_tunnel::UdpSocketThreadStopped>()
{
if e.any_is::<firezone_tunnel::UdpSocketThreadStopped>() {
return Err(e);
}

View File

@@ -6,7 +6,7 @@
use std::process::ExitCode;
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, ErrorExt, Result, bail};
use clap::{Args, Parser};
use controller::Failure;
use firezone_gui_client::{controller, deep_link, elevation, gui, logging, settings};
@@ -169,28 +169,25 @@ fn try_main(
return Err(anyhow);
}
if anyhow.root_cause().is::<gui::AlreadyRunning>() {
if anyhow.any_is::<gui::AlreadyRunning>() {
return Ok(());
}
if anyhow.root_cause().is::<gui::NewInstanceHandshakeFailed>() {
if anyhow.any_is::<gui::NewInstanceHandshakeFailed>() {
show_error_dialog(
"Firezone is already running but not responding. Please force-stop it first.",
)?;
return Err(anyhow);
}
if anyhow
.root_cause()
.is::<firezone_gui_client::ipc::NotFound>()
{
if anyhow.any_is::<firezone_gui_client::ipc::NotFound>() {
show_error_dialog(
"Couldn't find Firezone Tunnel service. Is the service running?",
)?;
return Err(anyhow);
}
if anyhow.root_cause().is::<controller::FailedToReceiveHello>() {
if anyhow.any_is::<controller::FailedToReceiveHello>() {
show_error_dialog(
"The Firezone Tunnel service is not responding. If the issue persists, contact your administrator.",
)?;

View File

@@ -8,7 +8,7 @@ use crate::{
updates, uptime,
view::{GeneralSettingsForm, SessionViewModel},
};
use anyhow::{Context, Result, anyhow, bail};
use anyhow::{Context, ErrorExt as _, Result, anyhow, bail};
use connlib_model::ResourceView;
use firezone_logging::FilterReloadHandle;
use firezone_telemetry::Telemetry;
@@ -669,8 +669,7 @@ impl<I: GuiIntegration> Controller<I> {
Ok(()) => {}
Err(error)
if error
.root_cause()
.downcast_ref::<auth::Error>()
.any_downcast_ref::<auth::Error>()
.is_some_and(|e| matches!(e, auth::Error::NoInflightRequest)) =>
{
tracing::debug!("Ignoring deep-link; no local state");

View File

@@ -2,7 +2,7 @@ use crate::{
ipc::{self, SocketId},
logging,
};
use anyhow::{Context as _, Result, bail};
use anyhow::{Context as _, ErrorExt as _, Result, bail};
use atomicwrites::{AtomicFile, OverwriteBehavior};
use backoff::ExponentialBackoffBuilder;
use connlib_model::ResourceView;
@@ -399,7 +399,7 @@ impl<'a> Handler<'a> {
if let Some(e) = result
.as_ref()
.err()
.and_then(|e| e.root_cause().downcast_ref::<io::Error>())
.and_then(|e| e.any_downcast_ref::<io::Error>())
{
tracing::debug!("Still cannot connect to Firezone: {e}");
@@ -534,7 +534,7 @@ impl<'a> Handler<'a> {
if let Some(e) = result
.as_ref()
.err()
.and_then(|e| e.root_cause().downcast_ref::<io::Error>())
.and_then(|e| e.any_downcast_ref::<io::Error>())
{
tracing::debug!(
"Encountered IO error when connecting to portal, most likely we don't have Internet: {e}"

View File

@@ -55,7 +55,7 @@ aya-log = { workspace = true }
ebpf-shared = { workspace = true, features = ["std"] }
[target.'cfg(target_os = "linux")'.build-dependencies]
anyhow = "1"
anyhow = { workspace = true }
aya-build = { workspace = true }
[dev-dependencies]