Skip to content

Releases: oswaldom-code/rhttp

rhttp v0.2.0 — NXDOMAIN no longer retried

Choose a tag to compare

@oswaldom-code oswaldom-code released this 05 Aug 15:57
68e909d

Second release of rhttp. One correctness fix in the error taxonomy and one allocation removed from the hot path. Still zero dependencies, still Go 1.21+.

Permanent DNS failures are no longer retried

ErrKindDNS was retryable unconditionally, so DefaultIsRetryable retried NXDOMAIN — a failure that is permanent by definition. A misspelled hostname or a DNS entry removed during a migration burned the entire attempt budget plus the full backoff schedule on an outcome that was already decided at the first lookup. The resolver had been reporting net.DNSError.IsNotFound == true the whole time; nothing read it.

Against this-host-does-not-exist.invalid with MaxAttempts: 4 and a 100 ms constant backoff:

v0.1.0 v0.2.0
Classify(err).Kind dns dns_not_found
Kind.IsRetryable() true false
transport attempts 4 1
wall clock 304 ms 0 s

The fix lives in the classification, not in the retry predicate. classifyError now consults IsNotFound and returns a distinct ErrKindDNSNotFound that is excluded from IsRetryable — which means all three public paths agree at once: DefaultIsRetryable, the package-level IsRetryable(err), and Classify(err).Kind.IsRetryable(). Special-casing the error inside the retry middleware would have left the other two still answering true.

ErrKindDNS was deliberately not flipped wholesale. DNS timeouts already classify as ErrKindTimeout, so what remains under ErrKindDNS is a genuine mix — SERVFAIL and friends stay retryable, as they should. Same distinction the AWS SDK for Go v2 and gRPC-Go make.

Lower allocation floor

The Timeout middleware deep-copied the request when all it needed was to attach a context. The copy was redundant: Do already clones the caller's request before the chain runs, so mid-chain independence protected nobody — it just duplicated the struct, the URL and the header map on every request.

Benchmark v0.1.0 v0.2.0
Full middleware stack 13 allocs / 1589 B 11 allocs / 1304 B
Timeout only 12 allocs 10 allocs
As a wrapper vs competitors 12 allocs 10 allocs
End-to-end (loopback) 76 allocs 74 allocs

Measured 2026-08-05, linux/amd64, i7-1255U, min of 5 runs. Full tables and methodology: benchmarks/REPORT.md.

Upgrading

Minor rather than patch, because behavior changes without you touching a line:

  • NXDOMAIN is no longer retried. If you were relying on those retries, they are gone. This is the point of the release.
  • IsDNS(err) now matches both DNS kinds, transient and permanent. It stays true for NXDOMAIN — an NXDOMAIN is still a DNS failure, and making it false would have broken existing callers silently. Use the new IsDNSNotFound(err) when you specifically want the permanent case.
  • ErrKindDNSNotFound is appended last in the ErrorKind block, so the numeric values shipped in 0.1.0 do not shift.

No API was removed. If you already narrowed retries with a custom IsRetryable, nothing changes for you.

Quality

  • build, vet and test -race green on Go 1.21 (the go.mod minimum), 1.22 and 1.23 in CI.
  • New tests cover NXDOMAIN classification, the *url.Error → *net.OpError → *net.DNSError chain as the error actually arrives from the transport, transient-DNS-stays-retryable, the retry attempt-count regression, and the invariant that Do shields the caller's request from middleware mutation.

Install

go get github.com/oswaldom-code/rhttp@v0.2.0

Full changelog: CHANGELOG.md

rhttp v0.1.0 — resilient HTTP client, zero dependencies

Choose a tag to compare

@oswaldom-code oswaldom-code released this 25 Jul 11:49
063b7d6

First public release of rhttp: a production-grade HTTP client for Go with built-in resiliency patterns and zero dependencies — Go standard library only.

Highlights

  • Middleware architecture — every capability is an http.RoundTripper decorator, composed with New(WithMiddleware(...)). Writing your own middleware is a one-liner with the exported RoundTripperFunc.
  • ResiliencyTimeout, Retry with pluggable backoff, CircuitBreaker (single-probe half-open, generation-gated state machine), and RateLimit (token bucket).
  • Retry-After aware — backoff strategies receive the response that triggered the retry; the WithRetryAfter decorator honors the Retry-After header (delay-seconds or HTTP-date) on 429/503.
  • Shared circuit stateNewCircuitBreaker lets several clients trip together against the same dependency, with observable State().
  • ObservabilityLogging and Metrics middleware; PathNormalizer bounds metrics label cardinality.
  • Fluent builderclient.R() with JSON/XML/form/reader bodies, path and query parameters. Bodies up to 10 MB are buffered so retries can rewind them; larger bodies stream and are sent exactly once.
  • Error classificationClassify, IsRetryable, IsTimeout, IsConnection and friends, plus the opinionated DecodeJSON helper.

Performance

Measured 2026-07-25 (linux/amd64, i7-1255U, min of 5 runs; see the README for methodology and caveats):

ns/op allocs/op
Full middleware stack (no-op transport) 1030 13
As a wrapper vs equivalently configured competitors 910 12
net/http floor (timeout only, no retry) 1750 26
Rate limiter check 48 0
Backoff strategies <10 0

Loopback numbers amplify relative differences — against a real network (0.5–500 ms) every client performs the same for practical purposes. Full tables and reproduction steps: benchmarks/REPORT.md.

Quality

  • 95.2% statement coverage on the root package; race-clean suite across Go 1.21–1.23 in CI.
  • Middleware interaction semantics (timeout budgets, per-attempt deadlines, breaker cutting retries) pinned by tests.

Install

go get github.com/oswaldom-code/rhttp@v0.1.0

Full changelog: CHANGELOG.md