Skip to content

Fix issues found during the connection-reuse review - #24

Merged
jonathan-kosgei merged 5 commits into
masterfrom
fix-noticed-issues
Nov 3, 2026
Merged

jonathan-kosgei merged 5 commits into
masterfrom
fix-noticed-issues

Conversation

@jonathan-kosgei

@jonathan-kosgei jonathan-kosgei commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follow-up to #23, fixing what that pull request listed under "Noticed, not changed": LookupFields never sent its fields parameter, a read error on an error response appeared twice in the error text, there was no way to put an overall time limit on a request while keeping the library's transport, doc.go pointed issue reports at the repository's former location, Scores was undocumented, and the CI matrix exercised Go 1.24 only. All changes are additive: no existing signature or default behaviour changes.

Changes

  • LookupFields sends the fields parameter. LookupFieldsWithContext appended ?fields=... to the URL, but newGetRequestWithContext then replaced the whole query string with the API key, so the API always returned the full response. The request builder now takes extra query parameters as a url.Values and encodes them together with api-key, which always takes precedence; the single-IP lookups share one request path instead of two copies of the response handling. Users of LookupFields now receive the partial response they asked for.
  • Body read errors are reported once. The three errors.Wrapf(err, "... %q: %s", resp.Status, err) calls formatted the read error into the message they also wrapped, so Error() showed it twice; it now reads failed to read body from response with status code "500 Internal Server Error": unexpected EOF.
  • New option WithTimeout(d time.Duration) sets an overall time limit for every request (http.Client.Timeout) while keeping the library's connection pool and HTTP/2 transport. It is applied after all other options, so it also covers an *http.Client given with WithHTTPClient in either order: that client is copied with Timeout set and the caller's value is left untouched; WithTimeout(0) means no limit. A request over the limit fails with an error whose Timeout() reports true, and bulk lookups are never replayed after a timeout. The README gains a "Timeouts" section and the package documentation states the default.
  • Documentation: doc.go points issue reports at github.com/ipdata/go/issues; Scores has a doc comment and the Threat comment names its Blocklists and Scores fields; the README "Nested Types" section lists Scores.
  • CI: Go 1.25, 1.26 and 1.27 join the test matrix, so current toolchains are tested as well (until now every job ended up on Go 1.24, see below).

Not changed

  • The Go 1.21, 1.22 and 1.23 matrix entries stay: go.mod requires 1.24.7, so these jobs switch to the 1.24.7 toolchain through GOTOOLCHAIN=auto and test the same thing as the 1.24 job; dropping them is a matrix reduction left to the maintainers.
  • No default request timeout: setting http.Client.Timeout by default would cut off existing callers' long-running requests, so the limit is opt-in through WithTimeout, and requests without it stay bounded by their context only.

Verification

Local, go1.24.7 linux/amd64, all on the final tree:

  • gofmt -l . prints nothing.
  • go vet ./... clean; go mod verify reports all modules verified.
  • go test -race -covermode atomic -cover -v ./...: ok github.com/ipdata/go, coverage 95.2% of statements (88.8% before); 28 top-level tests and 68 subtests pass, 0 fail.
  • go run honnef.co/go/tools/cmd/staticcheck@latest ./... (staticcheck 2026.2.2, 0.8.2): no findings.
  • go vet ./... and go test -race -covermode atomic -cover ./... repeated with GOTOOLCHAIN=go1.25.14, GOTOOLCHAIN=go1.26.10 and GOTOOLCHAIN=go1.27.3: ok on all three.
  • The new tests (Test_client_LookupFields, TestClient_truncatedErrorResponse, TestNewClient_WithTimeout, TestClient_WithTimeout_request) with -race -count=10: ok.
  • What the new tests cover: the request the test server receives carries fields=ip,country_name and api-key (and no fields parameter when none are given) and the partial response is decoded; newGetRequestWithContext encodes the parameters, keeps api-key in charge and leaves the caller's values unchanged; a server declaring a longer Content-Length than it sends drives the body read error for Lookup, RawLookup and BulkLookup; WithTimeout with the default and a custom *http.Client in both option orders, WithTimeout(0), and lookups against a server that answers too late.
  • CI: run https://github.com/ipdata/go/actions/runs/41640758084 (CI Update client to support latest API behaviors / data; v0.5.0 #7, head 08f7c71) completed with conclusion success: Test (Go 1.21), Test (Go 1.22), Test (Go 1.23), Test (Go 1.24), Test (Go 1.25), Test (Go 1.26), Test (Go 1.27) and Static Analysis all succeeded.

🤖 Generated with Claude Code

The three errors.Wrapf calls that wrap a failure to read the body of an
error response also formatted that failure into the message, so the
read error appeared twice in Error(). Wrap it once, and add a test that
drives the read error with a server declaring a longer Content-Length
than it sends.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
LookupFieldsWithContext appended "?fields=..." to the URL, but
newGetRequestWithContext then replaced the whole query string with the
API key, so the fields never reached the API and every LookupFields call
received the full response.

newGetRequestWithContext now takes the extra query parameters as a
url.Values and encodes them together with the API key, which always
takes precedence. LookupFields passes the comma-joined field list that
way. The single-IP lookups share one request path (rawLookup, lookup)
instead of a second copy of the response handling.

Tests assert that the request the server receives carries fields and
api-key (and no fields parameter when none were given), that the partial
response is decoded, and that newGetRequestWithContext encodes the
parameters without modifying the caller's values.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The default *http.Client bounds dialling and the TLS handshake but puts
no limit on waiting for or reading the response, so without a context
deadline a request can wait indefinitely. WithTimeout(d) sets Timeout on
the *http.Client a Client uses. It is applied after all other options,
so it also covers an *http.Client given with WithHTTPClient, in either
order: that client is copied with the Timeout set and the caller's value
is left untouched. Without the option nothing changes.

Tests cover both constructors, the option order, the untouched custom
client, WithTimeout(0), and lookups against a server that answers too
late failing with an error whose Timeout() reports true. The README and
the package documentation describe the default and the option.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
doc.go pointed issue reports at the repository's former location; it
now names github.com/ipdata/go/issues. Scores had no doc comment and the
Threat comment did not mention its Blocklists and Scores fields; both
are documented now, and the README's "Nested Types" section gains the
Scores entry it was missing.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
go.mod requires Go 1.24.7, so the 1.21, 1.22 and 1.23 jobs switch to
the 1.24.7 toolchain through GOTOOLCHAIN=auto and the matrix exercised
only Go 1.24. Add the releases since then so the tests also run on
current toolchains; the existing entries are kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jonathan-kosgei
jonathan-kosgei merged commit 9029ae4 into master Nov 3, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants