resolver: a query cut short by the caller's context records no reason (closes #298)
check / check (push) Waiting to run

When the caller's context ends, the next query returns
ErrContextCanceled before anything is sent. querySingleType recorded
that as a network error, which outranks a timeout, so a nameserver that
had only timed out could be reported as "network error: context
canceled". It now records no reason for it.

A try the context's deadline cuts short ends as a timeout. When the
context has ended before a further try, tryExchange now returns that
try's error instead of ErrContextCanceled, so such a nameserver is
reported as timed out, not as answering.

Model: opus-5-5
This commit is contained in:
2026-10-15 17:23:31 +00:00
parent 06ae357165
commit 9f4cf7c6c4
4 changed files with 109 additions and 6 deletions
+13 -2
View File
@@ -62,7 +62,10 @@ func checkCtx(ctx context.Context) error {
}
// tryExchange sends msg to addr over UDP, up to maxRetries times, until
// a reply comes. Every try waits the same queryTimeoutDuration.
// a reply comes. Every try waits the same queryTimeoutDuration, or less
// when ctx's deadline comes first. When ctx has ended before a further
// try, it returns the error of the try already made, such as a timeout,
// or ErrContextCanceled when none was made.
func (r *Resolver) tryExchange(
ctx context.Context,
msg *dns.Msg,
@@ -74,7 +77,11 @@ func (r *Resolver) tryExchange(
for range maxRetries {
if checkCtx(ctx) != nil {
return nil, ErrContextCanceled
if err == nil {
return nil, ErrContextCanceled
}
return nil, err
}
resp, _, err = r.client.ExchangeContext(ctx, msg, addr)
@@ -863,6 +870,10 @@ func (r *Resolver) querySingleType(
if err != nil {
err = fmt.Errorf("query %s @%s: %w", hostname, nsIP, err)
}
case errors.Is(err, ErrContextCanceled):
// The caller's context ended before the query was sent.
// That says nothing about the nameserver, so no reason is
// recorded.
case isTimeout(err):
state.gotTimeout = true
case errors.Is(err, ErrRefused):