release: v0.2.0 - permanent DNS failures are no longer retried - #6
Merged
Conversation
added 5 commits
July 25, 2026 14:04
The banner (1200x630 SVG, dark ground with the Go cyan retry-ring mark) tops the README and doubles as the social preview source; the PNG export for the GitHub setting ships alongside. README fixes: the error kind example used ErrKindCancelled which does not compile (the constant is ErrKindCanceled), the roadmap still listed the removed per-host limiter and pre-release status, and DecodeJSON was missing from the fluent API examples.
… request The Timeout middleware deep-copied the request (req.Clone) when all it needed was to attach a new context. The copy was redundant: Do already clones the caller's request before the chain runs, so mid-chain independence protects nobody -- it only duplicated the struct, the URL and the header map on every request. Do's clone is the sole remaining shield for the caller's request, so add TestClient_Do_ShieldsCallerRequestFromMiddleware to pin that invariant: a middleware that sets a header must reach the transport without leaking the header back onto the caller's request. Measured (min of 5, linux/amd64, Go 1.24.1): AllMiddleware 13 -> 11 allocs, 1589 -> 1304 B/op WithTimeout 12 -> 10 allocs overhead tier 12 -> 10 allocs (comparative harness) e2e tier 76 -> 74 allocs Refresh benchmarks/REPORT.md and the README benchmark tables accordingly. Verified on Go 1.21 (go.mod minimum): build, vet and test -race all green.
Cut 0.2.0 rather than a patch: the release adds public API (ErrKindDNSNotFound, IsDNSNotFound) and changes observable behavior. Callers who relied on NXDOMAIN being retried, or on IsDNS matching only the transient kind, will see a difference without touching their code. Contents: - fix: NXDOMAIN classifies as the non-retryable ErrKindDNSNotFound - perf: Timeout uses WithContext, full stack 13 -> 11 allocs
main takes squash merges, so its 0c887e8 is the squash of develop's 049707e. Git sees two unrelated commits touching the same README lines and conflicts, even though the trees are byte-identical (both beed00c). Resolved in favour of develop, which is a strict superset: git cherry reports 0c887e8 as already applied, and the resulting tree matches develop's HEAD exactly. The only conflicting hunk was the roadmap Status line, v0.2.0 on develop against the stale v0.1.0 on main.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cuts v0.2.0. Two changes: a correctness fix in the error taxonomy, and a redundant allocation removed from the
Timeoutmiddleware.Minor rather than patch: the release adds public API and changes observable behavior. A caller who relied on NXDOMAIN being retried, or on
IsDNSmatching only the transient kind, will see a difference without touching their code.Fix: permanent DNS failures are no longer retried
ErrKindDNSwas retryable unconditionally, soDefaultIsRetryableretried NXDOMAIN — permanent by definition. A misspelled or decommissioned hostname burned the whole attempt budget plus the full backoff schedule on an outcome that was already decided at the first lookup. The resolver was reportingnet.DNSError.IsNotFound == truethe entire time; the signal was simply never read.Reproduction against
.invalid(reserved by RFC 2606, can never resolve),MaxAttempts: 4, constant backoff 100 ms:Kinddnsdns_not_foundKind.IsRetryable()truefalseWhy the kind was split rather than the predicate patched
classifyErrornow consultsIsNotFoundand returns a distinctErrKindDNSNotFound, excluded fromIsRetryable.retry.gois untouched.DefaultIsRetryablealready delegates toKind.IsRetryable(), so fixing the classification fixes all three public paths at once —DefaultIsRetryable, the package-levelIsRetryable(err), andClassify(err).Kind.IsRetryable(). Special-casing the error inside the retry predicate would have left the other two still reportingtruefor NXDOMAIN, and the taxonomy is a headline feature of this library, not an advisory hint.ErrKindDNSis deliberately not flipped wholesale to non-retryable: DNS timeouts already classify asErrKindTimeout(thenet.Error.Timeout()check runs first), so what remains underErrKindDNSis a genuine mix. SERVFAIL and friends (IsTemporary) stay retryable.Same approach as the AWS SDK for Go v2 (
aws/retry/retryable_error.go) and gRPC-Go's DNS resolver.API
ErrKindDNSNotFound, appended last in theErrorKindblock so the numeric values shipped in 0.1.0 do not shift.IsDNSNotFound(err).IsDNS(err)now matches both DNS kinds. An NXDOMAIN is still a DNS failure — makingIsDNSfalse for it would silently break existing callers on upgrade, in the worst possible direction.Perf:
Timeoutattaches its context without cloningtimeout.godeep-copied the request when all it needed was to attach a new context. The copy was redundant:Doalready clones the caller's request before the chain runs, so mid-chain independence protects nobody — it only duplicated the struct, the URL and the header map on every request.Measured, min of 5, linux/amd64, Go 1.24.1:
AllMiddlewareWithTimeoutDo's clone is now the sole shield for the caller's request and nothing tested it, so this addsTestClient_Do_ShieldsCallerRequestFromMiddleware: a middleware that sets a header must reach the transport without the header leaking back onto the caller's request.benchmarks/REPORT.mdregenerated viamake report; README benchmark tables updated to match.Verification
go build,go vet,go test -racegreen on Go 1.21 (thego.modminimum) and on Go 1.24.1.golangci-lintclean,gofmtclean.*url.Error → *net.OpError → *net.DNSErrorchain as the error actually arrives, transient-DNS-stays-retryable,IsDNSNotFound, the retry-attempt-count regression case, and the caller-request shield.Note on the merge commit
275d98areconcilesmainintodevelop.maintakes squash merges, so its0c887e8is the squash of049707e— byte-identical trees (beed00c), different SHAs, guaranteed conflict. Resolved in develop's favour after confirminggit diff --cached HEADwas empty, i.e. the merge changes nothing in develop.