From f789993f24dbcb493fc6ac8323ff635aa529253d Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Sat, 12 Sep 2026 05:57:25 +0300 Subject: [PATCH] [resolver] answer unverified handles instead of erroring --- src/resolver.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/resolver.rs b/src/resolver.rs index 42ac3f5..ad78777 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -46,6 +46,9 @@ impl From for ResolverError { Self::Ratelimited } IdentityErrorKind::Transport(msg) => Self::Transport(msg.clone()), + // a timeout is the network failing to answer, not an answer. keeping it + // out of `Generic` is what lets callers read `Generic` as a verdict. + IdentityErrorKind::Timeout => Self::Transport("timed out".into()), _ => Self::Generic(e.into()), } } @@ -240,10 +243,19 @@ impl Resolver { } /// returns `true` if the given handle bi-directionally resolves to `did`. + /// + /// a handle nothing resolves to is an unverified handle, not an outage of this + /// service, so it answers `false` instead of erroring: the caller's fallback is + /// `handle.invalid`, which is the spec's representation for exactly this + /// (atproto.com/specs/handle#invalid-handles). transient failures stay errors, + /// because an unanswered question is not a negative answer. pub async fn verify_handle(&self, did: &Did, handle: &Handle) -> Result { let id = AtIdentifier::Handle(handle.clone()); - let resolved_did = self.resolve_did(&id).await?; - Ok(resolved_did.as_str() == did.as_str()) + match self.resolve_did(&id).await { + Ok(resolved_did) => Ok(resolved_did.as_str() == did.as_str()), + Err(ResolverError::Generic(_)) => Ok(false), + Err(e) => Err(e), + } } } @@ -256,6 +268,24 @@ mod tests { use super::*; use axum::{Router, http::header, response::IntoResponse}; + #[test] + fn resolution_verdicts_stay_apart_from_transient_failures() { + assert!(matches!( + ResolverError::from(IdentityError::handle_resolution_exhausted()), + ResolverError::Generic(_) + )); + assert!(matches!( + ResolverError::from(IdentityError::timeout()), + ResolverError::Transport(_) + )); + assert!(matches!( + ResolverError::from(IdentityError::http_status( + reqwest::StatusCode::TOO_MANY_REQUESTS + )), + ResolverError::Ratelimited + )); + } + async fn spawn_doc_server(body: String) -> Url { let app = Router::new().fallback(move || { let body = body.clone(); -- 2.51.2