diff --git a/.golangci.yml b/.golangci.yml index bd6fb4a..7e71eaf 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -19,6 +19,8 @@ linters: - unconvert - unparam - unused + - modernize + - testifylint settings: goconst: min-len: 2 diff --git a/benchmarks.go b/benchmarks.go index 24e1dea..e422d0e 100644 --- a/benchmarks.go +++ b/benchmarks.go @@ -156,10 +156,7 @@ func (b *Benchmarks) Stats(interval time.Duration) BenchmarkStats { } // ensure we calculate rate based on actual interval - actualInterval := fnInterval.Sub(stInterval) - if actualInterval < time.Second { - actualInterval = time.Second - } + actualInterval := max(fnInterval.Sub(stInterval), time.Second) return BenchmarkStats{ Requests: requests, diff --git a/go.mod b/go.mod index 092cd3c..412bc09 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,12 @@ go 1.24.0 require ( github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.46.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.38.0 // indirect + golang.org/x/sys v0.39.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 477304d..9f894fb 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/gzip.go b/gzip.go index a7328b0..67cac26 100644 --- a/gzip.go +++ b/gzip.go @@ -20,7 +20,7 @@ var gzDefaultContentTypes = []string{ } var gzPool = sync.Pool{ - New: func() interface{} { return gzip.NewWriter(io.Discard) }, + New: func() any { return gzip.NewWriter(io.Discard) }, } type gzipResponseWriter struct { diff --git a/logger/logger.go b/logger/logger.go index d93342c..df40932 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -32,7 +32,7 @@ type Middleware struct { // Backend is logging backend type Backend interface { - Logf(format string, args ...interface{}) + Logf(format string, args ...any) } type logParts struct { @@ -51,7 +51,7 @@ type logParts struct { type stdBackend struct{} -func (s stdBackend) Logf(format string, args ...interface{}) { +func (s stdBackend) Logf(format string, args ...any) { log.Printf(format, args...) } diff --git a/middleware.go b/middleware.go index c130a46..59e9f06 100644 --- a/middleware.go +++ b/middleware.go @@ -36,14 +36,17 @@ func AppInfo(app, author, version string) func(http.Handler) http.Handler { return f } -// Ping middleware response with pong to /ping. Stops chain if ping request detected +// Ping middleware response with pong to /ping. Stops chain if ping request detected. +// Handles both GET and HEAD methods - HEAD returns headers only without body, +// which is useful for lightweight health checks by monitoring tools. func Ping(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - - if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") { + if (r.Method == "GET" || r.Method == "HEAD") && strings.HasSuffix(strings.ToLower(r.URL.Path), "/ping") { w.Header().Set("Content-Type", "text/plain") w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("pong")) + if r.Method == "GET" { + _, _ = w.Write([]byte("pong")) + } return } next.ServeHTTP(w, r) diff --git a/middleware_test.go b/middleware_test.go index fe2da88..18f9ef7 100644 --- a/middleware_test.go +++ b/middleware_test.go @@ -46,7 +46,6 @@ func TestMiddleware_AppInfo(t *testing.T) { } func TestMiddleware_Ping(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, err := w.Write([]byte("blah blah")) require.NoError(t, err) @@ -54,21 +53,51 @@ func TestMiddleware_Ping(t *testing.T) { ts := httptest.NewServer(Ping(handler)) defer ts.Close() - resp, err := http.Get(ts.URL + "/ping") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - defer resp.Body.Close() - b, err := io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, "pong", string(b)) + t.Run("GET returns pong", func(t *testing.T) { + resp, err := http.Get(ts.URL + "/ping") + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "text/plain", resp.Header.Get("Content-Type")) + b, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "pong", string(b)) + }) - resp, err = http.Get(ts.URL + "/blah") - require.Nil(t, err) - assert.Equal(t, 200, resp.StatusCode) - defer resp.Body.Close() - b, err = io.ReadAll(resp.Body) - assert.NoError(t, err) - assert.Equal(t, "blah blah", string(b)) + t.Run("HEAD returns 200 with no body", func(t *testing.T) { + req, err := http.NewRequest(http.MethodHead, ts.URL+"/ping", http.NoBody) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + assert.Equal(t, "text/plain", resp.Header.Get("Content-Type")) + b, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Empty(t, b, "HEAD should return empty body") + }) + + t.Run("POST passes to next handler", func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, ts.URL+"/ping", http.NoBody) + require.NoError(t, err) + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + b, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "blah blah", string(b)) + }) + + t.Run("other paths pass to next handler", func(t *testing.T) { + resp, err := http.Get(ts.URL + "/blah") + require.NoError(t, err) + defer resp.Body.Close() + assert.Equal(t, http.StatusOK, resp.StatusCode) + b, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.Equal(t, "blah blah", string(b)) + }) } func TestMiddleware_Recoverer(t *testing.T) { diff --git a/realip/real.go b/realip/real.go index ad10149..cb492b1 100644 --- a/realip/real.go +++ b/realip/real.go @@ -58,8 +58,7 @@ func Get(r *http.Request) (string, error) { // check X-Forwarded-For, find leftmost public IP if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - addresses := strings.Split(xff, ",") - for _, addr := range addresses { + for addr := range strings.SplitSeq(xff, ",") { ip := strings.TrimSpace(addr) if parsedIP := net.ParseIP(ip); isPublicIP(parsedIP) { return ip, nil diff --git a/rest.go b/rest.go index d8181f3..94b3e9d 100644 --- a/rest.go +++ b/rest.go @@ -13,7 +13,7 @@ import ( type JSON map[string]any // RenderJSON sends data as json -func RenderJSON(w http.ResponseWriter, data interface{}) { +func RenderJSON(w http.ResponseWriter, data any) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(true) @@ -35,9 +35,8 @@ func RenderJSONFromBytes(w http.ResponseWriter, r *http.Request, data []byte) er } // RenderJSONWithHTML allows html tags and forces charset=utf-8 -func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) error { - - encodeJSONWithHTML := func(v interface{}) ([]byte, error) { +func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v any) error { + encodeJSONWithHTML := func(v any) ([]byte, error) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(false) @@ -55,7 +54,7 @@ func RenderJSONWithHTML(w http.ResponseWriter, r *http.Request, v interface{}) e } // renderJSONWithStatus sends data as json and enforces status code -func renderJSONWithStatus(w http.ResponseWriter, data interface{}, code int) { +func renderJSONWithStatus(w http.ResponseWriter, data any, code int) { buf := &bytes.Buffer{} enc := json.NewEncoder(buf) enc.SetEscapeHTML(true)