feat: add SetResolver and SetHosts for custom DNS (#169) - #516
feat: add SetResolver and SetHosts for custom DNS (#169)#516ManuelReschke wants to merge 1 commit into
Conversation
imroc
left a comment
There was a problem hiding this comment.
Review Summary
Reviewed locally: go build ./... && go vet ./... && go test ./... — all 22 packages pass. All 13 new tests pass individually. CI green (Go 1.25.x + 1.26.x).
Design Assessment
SetResolver — Clean wrapper around SetDial with net.Dialer{Resolver: r}. Straightforward, no concerns.
SetHosts — Well-designed fail-closed static mapping:
- Map is copied on entry (caller mutation safety verified by
TestSetHostsIgnoresCallerMapMutation) - IDNA normalization + case-insensitive matching is correct
- IP-literal passthrough correctly skips the map (verified by
TestSetHostsIPLiteralPassthrough+ scoped IPv6 test) - Invalid IP values are stored separately and produce a clear error without DNS fallback
- Proxy rejection (
rejectProxyWithSetHosts) prevents proxy-side DNS from bypassing the static map — tested in both orderings (proxy before/after SetHosts) SetDialcorrectly resetsrejectProxyWithSetHosts = false(replaces the hosts dialer)
Global wrappers in client_wrapper.go follow the existing pattern.
Modified Code (internal/http2/transport.go)
This is the key file requiring human review. The changes restructure dialTLSWithContext from a simple if/else into three paths:
-
TLSHandshakeContextpath — now usest.dialTCP()instead ofzeroDialer.DialContext()(correct, picks up custom DialContext). Also adds a missingconn.Close()onSplitHostPortfailure (good fix). -
Custom DialContext TLS path (new) — dials TCP via
t.dialTCP(), then manually performs TLS handshake with timeout + trace handling. This mirrors HTTP/1persistConn.addTLSsemantics. The timeout/trace/error handling logic looks correct. -
Default
tls.Dialerpath — unchanged from original.
Upstream sync impact: The restructured dialTLSWithContext is a substantial divergence from upstream x/net/http2. During future upstream sync, this function will need careful manual preservation. The dialTCP helper is a clean abstraction that should be easy to maintain.
Modified Code (transport.go)
Minimal and additive — rejectProxyWithSetHosts field, the proxy check in connectMethodForRequest, and Clone copy. These are small req-specific additions that are easy to preserve during upstream sync.
Verdict
Code quality is high, test coverage is excellent (13 tests covering all edge cases). CI passes on both Go versions. However, since this PR modifies internal/http2/transport.go (vendored x/net/http2), it requires human review per the project policy on modified stdlib code. The dialTLSWithContext restructuring in particular needs maintainer judgment on the approach and upstream sync implications.
Leaving for maintainer review.
Add SetResolver and SetHosts for custom DNS on HTTP/1 and HTTP/2
Fixes #169.
Closes #168 (discussion).
Summary
Adds a simple way to customize DNS resolution for HTTP/1 and HTTP/2, addressing long DNS timeouts when crawling with a known host list.
Client.SetResolver(*net.Resolver)— use a customnet.Resolver(e.g. a specific DNS server) viaSetDial+net.Dialer.Client.SetHosts(map[string]string)— static hostname→IP mapping (hosts-file style). Unlisted hostnames fail immediately with"no such host"and do not hit the system resolver.SetResolver,SetHosts.DialContextis set, the HTTP/2 transport uses it for the underlying TCP dial (includingEnableForceHTTP2), with TLS handshake timeout/trace handling aligned to HTTP/1addTLS.Motivation
From discussion #168: crawlers that hit many domains pay for system DNS timeouts on dead hosts. A custom resolver or hosts map that can return
NOHOSTquickly covers the common case.SetDialalready allowed this, but a dedicated API is clearer and documents the intended pattern.API
Notes:
SetDial). HTTP/3 is unchanged.https://1.2.3.4/) skip the map and dial directly.SetDialTLSstill bypasses this dialer for HTTPS when set.SetDial,SetResolver,SetHosts, andSetUnixSocketreplace each other (last call wins).Tests
SetResolver: IP URL works with a broken custom resolver; hostname fails with that resolver’s error.SetHosts: successful HTTP/1 and forced HTTP/2 via mapped hostname."no such host".SetDialreplaces hosts dialer.Validation: