perf: prefer the DNS answer in handle resolution #209
No reviewers
Labels
No labels
breaking-change
bug
documentation
duplicate
enhancement
good first issue
help wanted
performance
question
wontfix
No milestone
No project
No assignees
5 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/ATProto.NET!209
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-193-dns-first"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #193. The owner approved this policy change in chat.
What changed
HandleResolvernow follows the handle spec's resolution best practice: "It is ok to attempt both resolution methods in parallel, and to use the first successful result available. If the two methods return conflicting results (aka, different DIDs), the DNS TXT result should be preferred."HandleResolutionTimeoutbudget.finally.did=records in DNS still fail withHandleConflict, which now means only that.Security note: an HTTPS answer that disagrees with DNS no longer fails resolution; DNS wins. Control of a domain's DNS already implies control of its web host (A records, ACME DNS-01), and a hijacked web host still cannot override a DNS answer. Bidirectional verification against the DID document is unchanged.
Live latency (cache disabled, median of 5, warm connections)
A web host that drops packets no longer holds a DNS-configured handle for the whole 5 s budget.
Docs (
did-resolution.md,oauth.md), theHandleConflictdoc and the CHANGELOG are updated. Tests: the conflict test now asserts that DNS wins, and a new test checks that a DNS answer doesn't wait for a hanging well-known.🤖 Generated with Claude Code
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
💬 Verdict: comment — notes only, nothing blocking.
The implementation is correct. DNS is awaited first and a null falls through to the already-running HTTPS task. The
finallycancels the budget and observeshttpsTaskwithSuppressThrowing, so there are no unobserved exceptions or leaked requests. A DNS conflict or caller cancellation still propagates. Only DNS-sourced answers can throwHandleConflictnow. The new test is a good regression for the hanging well-known.Remaining items, none blocking:
IHandleResolver.ResolveAsyncinIDidResolver.cs:49-50still saysHandleConflictis thrown "when the authorities disagree".IIdentityResolverin the same file (line 63) still says "or to conflicting ones". Both should say that DNS publishes more than one DID.docs/migrating-to-0.7.md:1445also describes the old disagreement behaviour, so check whether it needs a note.docs/did-resolution.mdhas an over-long line.🧠 claude-sonnet-5-5 · ⏱ 0m 18s · 4 turns · tokens: 18.9k in / 1.7k out (+70.7k cache-read)
📜 Full session transcript
@ -102,1 +101,3 @@await Task.WhenAll(httpsTask, dnsTask).ConfigureAwait(false);answer = (_dnsOverHttpsUrl is null ? null: await ResolveViaDnsAsync(handle, _dnsOverHttpsUrl, budget.Token, cancellationToken).ConfigureAwait(false))?? await httpsTask.ConfigureAwait(false);The logic is fine. Related stale docs:
IHandleResolver.ResolveAsyncinIDidResolver.cs(lines 49-50) still documentsHandleConflictas "when the authorities disagree", andIIdentityResolver(line 63) still says "or to conflicting ones". Update both to match the new meaning (more than one DID in DNS).✅ Verdict: approve — no blocking issues found.
Clean implementation of the spec's DNS-preferred resolution policy. I verified the new try/finally control flow: a DNS answer or a DNS
HandleConflictcancels the budget so the moot HTTPS lookup stops, andConfigureAwaitOptions.SuppressThrowing(with the correct non-genericTaskcast) observes the HTTPS task without letting its cancellation mask the real outcome; caller cancellation still propagates from whichever lookup hits it. The two-record-DNSHandleConflictpath this PR narrows is exactly the failure the handle spec calls a failed resolution, and the removal of the HTTPS-vs-DNS conflict throw is the preference the spec explicitly endorses, so the security relaxation is sound — bidirectional DID-document verification, which is the real gate, is untouched.Two notes, neither blocking:
IHandleResolver.ResolveAsync(src/ATProtoNet/Identity/IDidResolver.cs, lines 49–50) still describesHandleConflictas "when the authorities disagree"; after this change it only means multiple DIDs in DNS. Worth a one-line follow-up.ResolveAsync_DnsAnswers_DoesNotWaitForHttpstest (1-minute budget) is the one that actually proves the early return; the pre-existing 300 ms-budget test would also have passed under the old code, so the new test is the load-bearing one. Good that it exists.Docs (
did-resolution.md,oauth.md), the enum doc, and the CHANGELOG are consistent with the new behavior, and the public API surface is unchanged.⏱ 1m 18s · 7 steps · $0.0136 · tokens: 31.8k in / 1.1k out (+82.9k cache-read) · Go: 5h 0% · wk 1% · mo 91%
📜 Full session transcript
🤖 Reviewing this PR — the review will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the review runs.
🤖 Reviewing this PR — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
✅ Verdict: approve — no blocking issues found.
The items from my last pass are mostly resolved.
IHandleResolver.ResolveAsyncandIIdentityResolver.ResolveAsyncnow sayHandleConflictmeans more than one DID in DNS.docs/did-resolution.mdnow says the DoH endpoint decides which DID a handle names, and that the DID document must still claim the handle back.docs/did-resolution.md: fixed.finallycancels the budget and observeshttpsTaskwithSuppressThrowing.One non-blocking item remains.
docs/migrating-to-0.7.md:1444-1445still says handle resolution fails closed when DNS and HTTPS disagree. That is now wrong, because DNS wins and only several DNS records conflict. Please reword it, since the page describes the 0.7 behaviour that this PR changes.🧠 claude-sonnet-5-5 · ⏱ 0m 9s · 3 turns · tokens: 18.7k in / 583 out (+42.9k cache-read)
📜 Full session transcript
✅ Verdict: approve — no blocking issues found.
Re-review: the follow-ups from the last pass are resolved. The
IHandleResolver/IIdentityResolverremarks inIDidResolver.csnow describeHandleConflictas several DIDs in DNS (in line with the enum doc, the OAuth error table and the CHANGELOG), anddid-resolution.mdnow states that the configurable DoH endpoint decides which DID a handle names — the trust-root narrowing flagged earlier is documented.Re-verified the implementation: the DNS answer (or a DNS
HandleConflict) propagates from the awaited DNS task; thefinallycancels the budget and observes the moot HTTPS lookup withConfigureAwaitOptions.SuppressThrowingon the non-genericTaskcast, so no unobserved exceptions; caller cancellation still surfaces; the answer cache and the multi-record DNS conflict path are unchanged. The spec explicitly endorses preferring the DNS result, and bidirectional DID-document verification is untouched, so the security relaxation is sound. The new hanging-well-known test is the load-bearing regression for the early return.One minor non-blocking leftover:
docs/migrating-to-0.7.md:1444–1445still says 0.7 handle resolution "fails closed on a conflict (DNS naming two DIDs, or DNS and HTTPS disagreeing)". Since this PR's CHANGELOG entry is in the same unreleased 0.7 section, that migration bullet now describes behavior 0.7 won't have — worth a one-line touch-up in a follow-up.⏱ 2m 23s · 7 steps · $0.0079 · tokens: 35.6k in / 1.0k out (+50.6k cache-read) · Go: 5h 1% · wk 1% · mo 91%
📜 Full session transcript