Skip to content

Commit be9e7a4

Browse files
Merge pull request #251 from RedisLabs/fix/small-fixes
Linting and bugfixes
2 parents 8ed0c07 + 8bf693d commit be9e7a4

File tree

31 files changed

+219
-74
lines changed

31 files changed

+219
-74
lines changed

.github/workflows/ci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,10 @@ jobs:
1717
go-version-file: go.mod
1818
cache: true
1919

20-
- name: Build
20+
- name: golangci-lint
21+
uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0
22+
with:
23+
version: v2.7.2
24+
25+
- name: Build and Test
2126
run: make

.golangci.yml

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
version: "2"
2+
3+
run:
4+
timeout: 5m
5+
tests: false
6+
# Use go.mod version, not toolchain version, to avoid mismatch with golangci-lint build
7+
go: '1.24'
8+
9+
formatters:
10+
enable:
11+
- goimports
12+
13+
linters:
14+
enable:
15+
# Default linters
16+
- errcheck
17+
- govet
18+
- ineffassign
19+
- staticcheck
20+
- unused
21+
22+
# High-value additional linters
23+
- bodyclose # HTTP response body not closed
24+
- durationcheck # suspicious duration multiplications
25+
- errorlint # proper error wrapping/comparison
26+
- gocritic # opinionated but useful checks
27+
- misspell # spelling mistakes
28+
- nilerr # returning nil with non-nil error
29+
- noctx # HTTP requests without context
30+
- unconvert # unnecessary type conversions
31+
32+
# Code quality
33+
- copyloopvar # detects loop variable capture bugs (Go 1.22+)
34+
- dupword # duplicate words in comments/strings
35+
- intrange # suggests using integer range loops (Go 1.22+)
36+
- makezero # finds slice bugs from non-zero initial length
37+
- perfsprint # suggests more efficient string formatting
38+
- prealloc # suggests slice preallocation
39+
- usestdlibvars # use stdlib constants (http.StatusOK, etc.)
40+
- wastedassign # finds wasted assignments
41+
42+
# Security
43+
- gosec # security checker
44+
45+
settings:
46+
gocritic:
47+
enabled-tags:
48+
- diagnostic
49+
- performance
50+
disabled-checks:
51+
- hugeParam # too noisy for this codebase
52+
53+
errorlint:
54+
errorf: true
55+
asserts: true
56+
comparison: true
57+
58+
gosec:
59+
excludes:
60+
- G101 # false positives on env var names like "SECRET_KEY"
61+
62+
dupword:
63+
ignore:
64+
- "active" # "active active" is intentional Redis terminology
65+
66+
exclusions:
67+
paths:
68+
- vendor
69+
rules:
70+
- linters:
71+
- gosec
72+
text: "G101" # additional rule-based exclusion for credential false positives

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,22 @@
22
All notable changes to this project will be documented in this file.
33
See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/).
44

5+
## 0.44.1 (Unreleased)
6+
7+
### Fixed:
8+
* Fixed nil pointer dereference when setting request headers before error check
9+
* Fixed race condition in rate limiter - mutex now released during sleep
10+
* Fixed context cancellation not being returned from rate limiter sleep
11+
* Fixed panic when database list page is empty
12+
* Fixed inconsistent receiver type in subscriptions List method
13+
* Fixed many other linting errors raised by new golangci-lint checks
14+
15+
### Changed:
16+
* `DeleteWithQuery` now accepts a query parameter instead of always passing nil
17+
18+
### Added:
19+
* Added golangci-lint to CI pipeline with comprehensive linting configuration
20+
521
## 0.44.0 (5th December 2025)
622

723
### Added:

client.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,8 +260,8 @@ func redactPasswords(data string) string {
260260
}
261261

262262
func escapePath(path string) string {
263-
escapedPath := strings.Replace(path, "\n", "", -1)
264-
escapedPath = strings.Replace(escapedPath, "\r", "", -1)
263+
escapedPath := strings.ReplaceAll(path, "\n", "")
264+
escapedPath = strings.ReplaceAll(escapedPath, "\r", "")
265265
return escapedPath
266266
}
267267

client_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package rediscloud_api
33
import (
44
"bytes"
55
"fmt"
6-
"io/ioutil"
6+
"io"
77
"net/http"
88
"net/url"
99
"strings"
@@ -71,7 +71,7 @@ func TestCredentialTripper_LogsRequestAndResponseBodies(t *testing.T) {
7171
ProtoMajor: 1,
7272
ProtoMinor: 1,
7373
Header: map[string][]string{},
74-
Body: ioutil.NopCloser(bytes.NewBufferString(`{"Here":"Value"}`)),
74+
Body: io.NopCloser(bytes.NewBufferString(`{"Here":"Value"}`)),
7575
ContentLength: 16,
7676
Host: "example.org",
7777
}
@@ -81,7 +81,7 @@ func TestCredentialTripper_LogsRequestAndResponseBodies(t *testing.T) {
8181
ProtoMajor: 1,
8282
ProtoMinor: 1,
8383
Header: map[string][]string{},
84-
Body: ioutil.NopCloser(bytes.NewBufferString(`{"Response":"Value"}`)),
84+
Body: io.NopCloser(bytes.NewBufferString(`{"Response":"Value"}`)),
8585
}
8686

8787
mockTripper.On("RoundTrip", request).Return(expected, nil)
@@ -195,7 +195,7 @@ func TestCredentialTripper_RedactPasswordFromBody(t *testing.T) {
195195
ProtoMajor: 1,
196196
ProtoMinor: 1,
197197
Header: map[string][]string{},
198-
Body: ioutil.NopCloser(bytes.NewBufferString(`{"password":"pass"}`)),
198+
Body: io.NopCloser(bytes.NewBufferString(`{"password":"pass"}`)),
199199
ContentLength: 19,
200200
Host: "example.org",
201201
}
@@ -205,7 +205,7 @@ func TestCredentialTripper_RedactPasswordFromBody(t *testing.T) {
205205
ProtoMajor: 1,
206206
ProtoMinor: 1,
207207
Header: map[string][]string{},
208-
Body: ioutil.NopCloser(bytes.NewBufferString(`{"password":"REDACTED"}`)),
208+
Body: io.NopCloser(bytes.NewBufferString(`{"password":"REDACTED"}`)),
209209
}
210210

211211
mockTripper.On("RoundTrip", request).Return(expected, nil)
@@ -252,7 +252,7 @@ func TestCredentialTripper_RedactPasswordFromNestedBody(t *testing.T) {
252252
ProtoMajor: 1,
253253
ProtoMinor: 1,
254254
Header: map[string][]string{},
255-
Body: ioutil.NopCloser(bytes.NewBufferString(`{"security": {"password":"pass", "global_password":"globalpass"}}`)),
255+
Body: io.NopCloser(bytes.NewBufferString(`{"security": {"password":"pass", "global_password":"globalpass"}}`)),
256256
ContentLength: 65,
257257
Host: "example.org",
258258
}
@@ -262,7 +262,7 @@ func TestCredentialTripper_RedactPasswordFromNestedBody(t *testing.T) {
262262
ProtoMajor: 1,
263263
ProtoMinor: 1,
264264
Header: map[string][]string{},
265-
Body: ioutil.NopCloser(bytes.NewBufferString(`{"security": {"password":"REDACTED", "global_password":"REDACTED"}}`)),
265+
Body: io.NopCloser(bytes.NewBufferString(`{"security": {"password":"REDACTED", "global_password":"REDACTED"}}`)),
266266
}
267267

268268
mockTripper.On("RoundTrip", request).Return(expected, nil)

go.mod

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ toolchain go1.25.5
77
require (
88
github.com/avast/retry-go/v4 v4.7.0
99
github.com/stretchr/testify v1.11.1
10-
golang.org/x/tools v0.39.0
10+
golang.org/x/tools v0.40.0
1111
)
1212

1313
require (
1414
github.com/davecgh/go-spew v1.1.1 // indirect
1515
github.com/pmezard/go-difflib v1.0.0 // indirect
1616
github.com/stretchr/objx v0.5.2 // indirect
17-
golang.org/x/mod v0.30.0 // indirect
18-
golang.org/x/sync v0.18.0 // indirect
19-
golang.org/x/sys v0.38.0 // indirect
20-
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 // indirect
17+
golang.org/x/mod v0.31.0 // indirect
18+
golang.org/x/sync v0.19.0 // indirect
19+
golang.org/x/sys v0.39.0 // indirect
20+
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc // indirect
2121
gopkg.in/yaml.v3 v3.0.1 // indirect
2222
)

go.sum

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
1010
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
1111
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
1212
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
13-
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
14-
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
15-
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
16-
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
17-
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
18-
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
19-
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54 h1:E2/AqCUMZGgd73TQkxUMcMla25GB9i/5HOdLr+uH7Vo=
20-
golang.org/x/telemetry v0.0.0-20251111182119-bc8e575c7b54/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
21-
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
22-
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
13+
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
14+
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
15+
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
16+
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
17+
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
18+
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
19+
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA=
20+
golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ=
21+
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
22+
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
2323
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
2424
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2525
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

internal/http_client.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ func (c *HttpClient) Delete(ctx context.Context, name, path string, requestBody
7171
return c.connectionWithRetries(ctx, http.MethodDelete, name, path, nil, requestBody, responseBody)
7272
}
7373

74-
func (c *HttpClient) DeleteWithQuery(ctx context.Context, name, path string, requestBody interface{}, responseBody interface{}) error {
75-
return c.connectionWithRetries(ctx, http.MethodDelete, name, path, nil, requestBody, responseBody)
74+
func (c *HttpClient) DeleteWithQuery(ctx context.Context, name, path string, query url.Values, requestBody interface{}, responseBody interface{}) error {
75+
return c.connectionWithRetries(ctx, http.MethodDelete, name, path, query, requestBody, responseBody)
7676
}
7777

7878
func (c *HttpClient) connectionWithRetries(ctx context.Context, method, name, path string, query url.Values, requestBody interface{}, responseBody interface{}) error {
@@ -88,7 +88,7 @@ func (c *HttpClient) connectionWithRetries(ctx context.Context, method, name, pa
8888
}
8989
var target *HTTPError
9090
if errors.As(err, &target) && target.StatusCode == http.StatusTooManyRequests {
91-
c.logger.Println(fmt.Sprintf("status code 429 received, request will be retried"))
91+
c.logger.Println("status code 429 received, request will be retried")
9292
return true
9393
}
9494
return false
@@ -126,14 +126,13 @@ func (c *HttpClient) connection(ctx context.Context, method, name, path string,
126126
}
127127

128128
request, err := http.NewRequestWithContext(ctx, method, u, body)
129-
130-
// The API expects this entry in the header in all requests.
131-
request.Header.Set("Content-Type", "application/json")
132-
133129
if err != nil {
134130
return fmt.Errorf("failed to create request to %s: %w", name, err)
135131
}
136132

133+
// The API expects this entry in the header in all requests.
134+
request.Header.Set("Content-Type", "application/json")
135+
137136
response, err := c.client.Do(request)
138137
if err != nil {
139138
return fmt.Errorf("failed to %s: %w", name, err)
@@ -149,7 +148,7 @@ func (c *HttpClient) connection(ctx context.Context, method, name, path string,
149148
}
150149
}
151150

152-
defer response.Body.Close()
151+
defer func() { _ = response.Body.Close() }()
153152

154153
if response.StatusCode > 299 {
155154
body, _ := io.ReadAll(response.Body)

internal/model.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func (e *Error) Error() string {
5252
return fmt.Sprintf("%s - %s: %s", redis.StringValue(e.Status), redis.StringValue(e.Type), redis.StringValue(e.Description))
5353
}
5454

55-
var errorStatusCode = regexp.MustCompile("^(\\d*).*$")
55+
var errorStatusCode = regexp.MustCompile(`^(\d*).*$`)
5656
var _ error = &Error{}
5757

5858
// TaskResponse is the high-level response when a Create/Update/Delete operation is in progress.

internal/rate_limit.go

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package internal
22

33
import (
44
"context"
5-
"fmt"
65
"sync"
76
"time"
87
)
@@ -59,12 +58,21 @@ func (rl *fixedWindowCountRateLimiter) Wait(ctx context.Context) error {
5958
windowEnd = rl.windowStart.Add(rl.period)
6059
}
6160

62-
if rl.count == rl.limit {
63-
delay := windowEnd.Sub(time.Now())
61+
if rl.count >= rl.limit {
62+
delay := time.Until(windowEnd)
63+
rl.mu.Unlock()
6464
err := sleepWithContext(ctx, delay)
65+
rl.mu.Lock()
6566
if err != nil {
6667
return err
6768
}
69+
// After sleeping, the window may have reset - recheck and reset count if needed
70+
now := time.Now()
71+
windowEnd = rl.windowStart.Add(rl.period)
72+
if now.After(windowEnd) {
73+
rl.count = 0
74+
rl.windowStart = &windowEnd
75+
}
6876
}
6977
rl.count++
7078
return nil
@@ -82,11 +90,12 @@ func sleepWithContext(ctx context.Context, d time.Duration) error {
8290
select {
8391
case <-ctx.Done():
8492
if !timer.Stop() {
85-
return fmt.Errorf("context expired before timer stopped")
93+
<-timer.C // Drain the timer channel to prevent leaks
8694
}
95+
return ctx.Err()
8796
case <-timer.C:
97+
return nil
8898
}
89-
return nil
9099
}
91100

92101
var _ RateLimiter = &fixedWindowCountRateLimiter{}

0 commit comments

Comments
 (0)