diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5f7c52..a327063 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,12 @@ jobs: - uses: actions/setup-go@v5 with: go-version: "1.26" + check-latest: true + - name: Install golangci-lint + # Build from source with the CI Go toolchain so the linter binary's Go + # version matches the module's `go 1.26` directive. golangci-lint refuses + # to run when it was built with an older Go than the target module, which + # the prebuilt action binaries currently are. + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 - name: golangci-lint - uses: golangci/golangci-lint-action@v6 - with: - version: latest + run: golangci-lint run ./... diff --git a/.golangci.yml b/.golangci.yml index 6d33253..7d241be 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -33,6 +33,14 @@ linters: - name: exported arguments: - disableStutteringCheck + exclusions: + rules: + # Test stub transporters return in-memory bodies (NopCloser / http.NoBody) + # that hold no resources, and many cases deliberately discard the response + # to assert an error. bodyclose stays enforced for non-test code. + - path: _test\.go + linters: + - bodyclose formatters: enable: diff --git a/auth/basic_test.go b/auth/basic_test.go index 015ce74..71fb790 100644 --- a/auth/basic_test.go +++ b/auth/basic_test.go @@ -17,7 +17,7 @@ import ( func okTransport(seen *http.Request) transporterFunc { return func(req *http.Request) (*http.Response, error) { *seen = *req - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil } } diff --git a/auth/bearer_test.go b/auth/bearer_test.go index bda1d25..d5d3d73 100644 --- a/auth/bearer_test.go +++ b/auth/bearer_test.go @@ -44,7 +44,7 @@ func TestBearerAttachesHeaderAndCaches(t *testing.T) { var seen string transport := transporterFunc(func(req *http.Request) (*http.Response, error) { seen = req.Header.Get(header.Authorization) - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil }) cred := &countingCredential{token: "abc123", exp: time.Now().Add(time.Hour)} @@ -90,7 +90,7 @@ func TestBearerPropagatesCredentialError(t *testing.T) { cred := errCredential{err: wantErr} pl := pipeline.New( transporterFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 200, Body: http.NoBody, Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Request: req}, nil }), auth.NewBearerTokenPolicy(cred), ) @@ -109,7 +109,7 @@ func TestBearerSharedCacheReusesToken(t *testing.T) { run := func(p *auth.BearerTokenPolicy) { transport := transporterFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil }) pl := pipeline.New(transport, p) req, _ := http.NewRequest(http.MethodGet, "https://api.example.test/", nil) @@ -142,7 +142,7 @@ func TestBearerRefetchesNearExpiryToken(t *testing.T) { cred := &countingCredential{token: "tok", exp: time.Now().Add(time.Minute)} pl := pipeline.New( transporterFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil }), auth.NewBearerTokenPolicy(cred, "scope"), ) diff --git a/auth/digest.go b/auth/digest.go index 13c7e71..db0f23f 100644 --- a/auth/digest.go +++ b/auth/digest.go @@ -4,7 +4,7 @@ package auth import ( - "crypto/md5" + "crypto/md5" //nolint:gosec // G501: MD5 is mandated by RFC 7616 Digest; it is a protocol primitive here, not used for security-sensitive hashing. "crypto/rand" "crypto/sha256" "encoding/hex" @@ -70,7 +70,7 @@ func (p *DigestAuthPolicy) Do(req *pipeline.Request) (*http.Response, error) { return resp, nil } if rerr := req.RewindBody(); rerr != nil { - return resp, nil // non-replayable body: cannot retry, surface the 401 + return resp, nil //nolint:nilerr // intentional: a non-replayable body cannot be retried, so the 401 response is surfaced unchanged. } nc := p.adopt(ch) hdr, herr := p.authorization(ch, nc, raw.Method, raw.URL.RequestURI()) diff --git a/client_test.go b/client_test.go index 62b9a5b..4041327 100644 --- a/client_test.go +++ b/client_test.go @@ -35,7 +35,7 @@ func (f transporterFunc) Do(req *http.Request) (*http.Response, error) { return func captureTransport(captured **http.Request) transporterFunc { return func(r *http.Request) (*http.Response, error) { *captured = r - return &http.Response{StatusCode: 200, Body: http.NoBody, Request: r}, nil + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Request: r}, nil } } diff --git a/example_test.go b/example_test.go index 101530a..dc90c02 100644 --- a/example_test.go +++ b/example_test.go @@ -10,7 +10,7 @@ import ( "net/http" "net/http/httptest" - "github.com/dexpace/go-sdk" + dexpace "github.com/dexpace/go-sdk" "github.com/dexpace/go-sdk/retry" ) @@ -18,7 +18,7 @@ import ( // standard *http.Request, read the response. func ExampleClient_Do() { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - fmt.Fprint(w, "pong") + _, _ = fmt.Fprint(w, "pong") })) defer srv.Close() diff --git a/idempotency/policy_test.go b/idempotency/policy_test.go index 2b53219..3d024f0 100644 --- a/idempotency/policy_test.go +++ b/idempotency/policy_test.go @@ -50,7 +50,7 @@ type transporterFunc func(*http.Request) (*http.Response, error) func (f transporterFunc) Do(req *http.Request) (*http.Response, error) { return f(req) } func okResp(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 200, Body: http.NoBody, Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Request: req}, nil } func runPolicy(t *testing.T, p *Policy, req *http.Request) *http.Request { diff --git a/instrumentation/tracing_policy_test.go b/instrumentation/tracing_policy_test.go index 45f4e98..119fb0c 100644 --- a/instrumentation/tracing_policy_test.go +++ b/instrumentation/tracing_policy_test.go @@ -20,7 +20,7 @@ type transporterFunc func(*http.Request) (*http.Response, error) func (f transporterFunc) Do(req *http.Request) (*http.Response, error) { return f(req) } func okResp(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil } type fakeSpan struct { diff --git a/logging/logging_test.go b/logging/logging_test.go index 169504a..85bd8e1 100644 --- a/logging/logging_test.go +++ b/logging/logging_test.go @@ -25,7 +25,7 @@ func TestLoggingRedactsQuerySecret(t *testing.T) { var buf bytes.Buffer logger := slog.New(slog.NewTextHandler(&buf, nil)) transport := transporterFunc(func(req *http.Request) (*http.Response, error) { - return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil + return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader("")), Request: req}, nil }) pl := pipeline.New(transport, logging.NewPolicy(logging.Options{Logger: logger})) diff --git a/pagination/strategies.go b/pagination/strategies.go index 79cca59..4d2dc49 100644 --- a/pagination/strategies.go +++ b/pagination/strategies.go @@ -40,7 +40,9 @@ func NewPageNumber[T any](startPage int, fetch func(ctx context.Context, page in // NewLinkHeader returns a Pager that follows RFC 8288 Link headers. fetch is // called with the next URL (empty for the first page) and returns the page's -// items and the HTTP response whose Link header carries the next URL. +// items and the HTTP response whose Link header carries the next URL. The Pager +// owns each returned response and closes its body after reading the Link header, +// so fetch must finish reading the body (to produce its items) before returning. // // The next URL is taken from the server-controlled Link header, so fetch should // validate or trust the URL's host before dialing it to avoid SSRF. @@ -50,7 +52,11 @@ func NewLinkHeader[T any](fetch func(ctx context.Context, url string) ([]T, *htt if err != nil { return Page[T]{}, err } - return Page[T]{Items: items, NextToken: NextLink(resp)}, nil + next := NextLink(resp) + if resp != nil && resp.Body != nil { + _ = resp.Body.Close() + } + return Page[T]{Items: items, NextToken: next}, nil } return New(tokenFetch, opts...) } diff --git a/pipeline/context_test.go b/pipeline/context_test.go index 7fbe98e..bd36271 100644 --- a/pipeline/context_test.go +++ b/pipeline/context_test.go @@ -42,7 +42,7 @@ func TestSetContextIgnoresNil(t *testing.T) { t.Parallel() p := pipeline.PolicyFunc(func(req *pipeline.Request) (*http.Response, error) { - req.SetContext(nil) // must not panic + req.SetContext(nil) //nolint:staticcheck // SA1012: intentionally nil to verify SetContext ignores a nil context (must not panic). return req.Next() }) pl := pipeline.New(transporterFunc(okResponse), p) diff --git a/pipeline/stage_test.go b/pipeline/stage_test.go index dd48d8c..8a5e880 100644 --- a/pipeline/stage_test.go +++ b/pipeline/stage_test.go @@ -26,7 +26,7 @@ func TestStagesAreOrdered(t *testing.T) { pipeline.StageLogging, } for i := 1; i < len(ordered); i++ { - if !(ordered[i-1] < ordered[i]) { + if ordered[i-1] >= ordered[i] { t.Fatalf("stage %d not less than stage %d", ordered[i-1], ordered[i]) } } diff --git a/transport/transport.go b/transport/transport.go index 4e6bd6c..3192886 100644 --- a/transport/transport.go +++ b/transport/transport.go @@ -94,7 +94,7 @@ func New(opts ...Option) *Transport { // Do performs the HTTP round-trip. It satisfies pipeline.Transporter. func (t *Transport) Do(req *http.Request) (*http.Response, error) { - return t.client.Do(req) + return t.client.Do(req) //nolint:gosec // G704: this is the SDK's HTTP transport; issuing the caller's own request is its sole purpose. } // defaultRoundTripper clones http.DefaultTransport so global state is untouched,