actix/actix-web · critical

failed to build Hickory DNS resolver

Error message

failed to build Hickory DNS resolver

What it means

This is a panic (via .expect) inside awc's resolver module when the `hickory-dns` Cargo feature is enabled. The resolver is constructed lazily in a thread-local OnceCell (connector.rs:1088-1093); on first use per thread it calls `TokioResolver::builder_with_config(cfg, opts).build().expect("failed to build Hickory DNS resolver")` (connector.rs:1104-1108). Note the code already handles a failed read_system_conf() by falling back to `ResolverConfig::default()` (connector.rs:1096-1102), so the panic is specifically from TokioResolver.build() failing — i.e. an error building the async resolver from a (possibly defaulted) config, not from parsing resolv.conf. The panic therefore indicates a lower-level runtime/provider/IO problem at resolver construction.

Solutions

  1. Disable the `hickory-dns` (and legacy `trust-dns`) feature and use awc's default resolver (getaddrinfo-based Resolver::default()), which has no build() step and no extra runtime dependency. This removes the panic entirely.
  2. Ensure the process runs inside a tokio multi-thread runtime (the hickory TokioRuntimeProvider requires it); if you use a single-threaded/current-thread runtime, switch to the default resolver or a proper tokio runtime.
  3. Pin compatible versions: keep hickory-resolver at the version awc expects (0.26.1) and ensure tokio is the version hickory 0.26 was built against; resolve Cargo.lock conflicts via `cargo update -p hickory-resolver`.
  4. If you must keep hickory-dns, provide a valid /etc/resolv.conf and verify the resolver can be built outside awc first (see validationCode) to isolate whether the failure is environmental.
  5. As a last resort, supply a custom Resolver (Resolver::custom(...)) to awc::Connector so the hickory build path is never taken.

Example fix

# before: awc depends on hickory for DNS
awc = { version = "4", features = ["hickory-dns"] }

# after: use the default getaddrinfo resolver (no hickory, no panic path)
awc = { version = "4" }

# or, in code, avoid the default hickory resolver by passing a custom one:
# let connector = awc::Connector::new().resolver(my_resolver);
Defensive patterns

Strategy: fallback

Validate before calling

// Validate that a hickory TokioResolver can be built BEFORE awc tries to,
// by attempting the same construction read_system_conf + build() sequence.
// Run this at startup when the `hickory-dns` feature is enabled.
use hickory_resolver::{
    config::{ResolverConfig, ResolverOpts},
    system_conf::read_system_conf,
    TokioResolver, async_resolver::TokioRuntimeProvider,
};

fn hickory_builds_ok() -> bool {
    let (cfg, opts) = read_system_conf()
        .unwrap_or((ResolverConfig::default(), ResolverOpts::default()));
    TokioResolver::builder_with_config(cfg, TokioRuntimeProvider::default())
        .with_options(opts)
        .build()
        .is_ok()
}

if !hickory_builds_ok() {
    panic!("hickory DNS resolver cannot be built; disable the hickory-dns feature or fix the runtime");
}

Type guard

// No type guard: feature-gated and panics at runtime inside a thread-local.
// Detect via the standalone build() probe above instead.
// null

Try / catch

// awc's resolver() panics inside a thread-local OnceCell; you cannot catch it
// directly because it runs lazily on first DNS use. Prevent it by constructing
// the resolver yourself and passing a custom Resolver, so the panic path is
// never taken:
use actix_tls::connect::Resolver;
use std::panic;

let client = match panic::catch_unwind(|| {
    // verify hickory builds; if it panics, fall back to default GAI resolver
    awc::Client::new()
}) {
    Ok(c) => c,
    Err(_) => {
        // explicit default resolver avoids the hickory code path entirely
        awc::Client::builder()
            .connector(awc::Connector::new()) // Resolver::default(), no hickory
            .finish()
    }
};
// Best practice: do NOT enable the hickory-dns feature unless you need it.

Prevention

When it happens

Trigger: Enabling awc's `hickory-dns` (or the deprecated `trust-dns`) feature and performing the first DNS resolution on a given worker thread, which triggers the thread-local OnceCell initialization and the TokioResolver::build() call. The panic surfaces as soon as a client tries to resolve a hostname and the cached resolver does not yet exist on that thread. read_system_conf failure is tolerated, so the trigger is build() itself returning Err.

Common situations: Containers where the tokio runtime provider for hickory cannot function correctly (missing network namespaces, restricted syscalls); version incompatibilities between hickory-resolver and the tokio runtime (e.g. awc's hickory-resolver 0.26.1 pinned against an incompatible tokio); misconfigured or absent /etc/resolv.conf combined with a build() that still errors; async runtimes not backed by tokio (hickory's TokioRuntimeProvider requires a tokio context); extremely locked-down environments where resolver background tasks cannot spawn.

Related errors


AI-assisted analysis of actix/actix-web@4d435abc28 (2026-08-09). Data as JSON: /api/errors/968e76e5cb8e86bc. Report an issue: GitHub.

Appendix: source

Thrown at awc/src/client/connector.rs:1108

        }

        // get from thread local or construct a new hickory dns resolver.
        HICKORY_DNS_RESOLVER.with(|local| {
            local
                .get_or_init(|| {
                    let (cfg, opts) = match read_system_conf() {
                        Ok((cfg, opts)) => (cfg, opts),
                        Err(err) => {
                            log::error!("Hickory DNS can not load system config: {err}");
                            (ResolverConfig::default(), ResolverOpts::default())
                        }
                    };

                    let resolver =
                        TokioResolver::builder_with_config(cfg, TokioRuntimeProvider::default())
                            .with_options(opts)
                            .build()
                            .expect("failed to build Hickory DNS resolver");

                    Resolver::custom(HickoryDnsResolver(resolver))
                })
                .clone()
        })
    }
}

#[cfg(feature = "dangerous-h2c")]
#[cfg(test)]
mod tests {
    use std::convert::Infallible;

    use actix_http::{HttpService, Request, Response, Version};
    use actix_http_test::test_server;
    use actix_service::ServiceFactoryExt as _;

    use super::*;

View on GitHub (pinned to 4d435abc28)