一个 rust 程序使用了 reqwest ,频繁地报错 error sending request for url ,调试过程记录一下。
reqwest 的 Error 使用 thiserror 接收
#[error(transparent)]
Reqwest(#[from] reqwest::Error),
在外层程序打印报错信息:
let source = e.source();
tracing::error!("{e}, source: {source:?}");
source 是:
source: Some(hyper_util::client::legacy::Error(Connect, ConnectError("dns error", Custom { kind: Uncategorized, error: "failed to lookup address information: Name has no usable address" })))
确定是 dns 查询问题,怀疑是优先查询了 ipv6 ,因为要查询的域名是禁用了 ipv6 地址解析的。reqwest 换用 hickory-dns ,得到报错信息:
source: Some(hyper_util::client::legacy::Error(Connect, ConnectError("dns error", ResolveError { kind: Proto(ProtoError { kind: NoRecordsFound { query: Query { name: Name("example.com."), query_type: AAAA, query_class: IN }, soa: Some(Record { name_labels: Name("example.com."), dns_class: IN, ttl: 10, rdata: SOA { mname: Name("ns.co.uk."), rname: Name("example.com."), serial: 1, refresh: 7200, retry: 900, expire: 1209600, minimum: 60 } }), ns: None, negative_ttl: Some(9), response_code: NoError, trusted: true, authorities: None } }) })))
于是确定了是查询 ipv6 导致的。
看 reqwest 是否提供配置项,找到代码:
/// Create a new resolver with the default configuration,
/// which reads from `/etc/resolve.conf`. If reading `/etc/resolv.conf` fails,
/// it fallbacks to hickory_resolver's default config.
/// The options are overridden to look up for both IPv4 and IPv6 addresses
/// to work with "happy eyeballs" algorithm.
fn new_resolver() -> TokioResolver {
let mut builder = TokioResolver::builder_tokio().unwrap_or_else(|err| {
log::debug!(
"hickory-dns: failed to load system DNS configuration; falling back to hickory_resolver defaults: {:?}",
err
);
TokioResolver::builder_with_config(
ResolverConfig::default(),
TokioConnectionProvider::default(),
)
});
builder.options_mut().ip_strategy = LookupIpStrategy::Ipv4AndIpv6;
builder.build()
}
果然是双栈查询,但是这个查询策略在 reqwest 中是无法修改的,那 hickory-dns 会不会读取系统的配置,从而改变这种行为呢?
修改 /etc/gai.conf,将 precedence ::ffff:0:0/96 100 这行的注释打开,还是在报错,可能是换成了 hickory-dns ,它并不读取这个配置文件。
再根据 gemini 的说法修改 /etc/resolv.conf ,加一行 options no-aaaa 。唉,可以了,不再报错了。
看 hickory-dns ,它用了 hickory-dns/resolv-conf 这个 crate,它里面定义的选项很多,其中有两个和 ipv6 解析有关: inet6, no-aaaa 。
inet6 只是一个启用开关,并没有提供 no-inet6 这种禁用的选项,那用且只能用 no-aaaa 选项了。
改完用了一段时间,发现报错又出现了。
reqwest 的 Client builder 提供了 dns_resolver 方法,可以自定义 dns 的解析。
将 reqwest hickory 这段代码复制过来,然后将查询策略设置为 LookupIpStrategy::Ipv4Only ,再次运行,这次的错误变成了:
Some(hyper_util::client::legacy::Error(Connect, ConnectError("dns error", ResolveError { kind: Proto(ProtoError { kind: NoRecordsFound { query: Query { name: Name("example.com."), query_type: A, query_class: IN }, soa: None, ns: None, negative_ttl: None, response_code: NoError, trusted: true, authorities: None } }) })))
通过这个报错,回顾一下查询策略,它提供这几种方式:
pub enum LookupIpStrategy {
Ipv4Only,
Ipv6Only,
Ipv4AndIpv6,
Ipv6thenIpv4,
Ipv4thenIpv6,
}
hickory-dns 的默认值是 Ipv4thenIpv6 , reqwest 的值是 Ipv4AndIpv6
也就是说,查询策略不是根本原因,ipv4 查询失败才是根本所在。通过观察,最近到上游 dns server 的网络不稳定,某段时间丢包率较高,从而导致 ipv4 查询失败的呢?
又进行了测试和对比,发现即使在国外VPS上用 drill 执行查询,仍然偶尔出现 The answer packet was truncated 的警告,将上游从 1.1.1.1 换成 8.8.8.8,运行一段时间,报错不再出现。
再挖这个警告,gemini给出的回复是
传统的 DNS 协议基于 UDP,且标准规定响应报文不能超过 512 字节,当服务器发现包太长发不出去时,会设置 DNS 报文头部的 TC (Truncated) 标志位。客户端收到带 TC 标志的包后,就会提示“报文被截断”。 为了解决 512 字节限制,引入了 EDNS(0) 扩展,允许 UDP 包最大达到 4096 字节。