fix(canary): replace //nolint pragmas with explicit error handling

The standing "no //nolint anywhere" rule (handoff anti-relitigation
set) was honored by Phases 2-4 but inherited template code in
core/database.go, middleware/ratelimit.go, and health/handler.go
carried 6 pragmas from the original template import. Replacing each
with proper error handling rather than silencing the linter:

core/database.go
  NewDatabase ping-failure path: propagates db.Close() error via
  multi-%w fmt.Errorf rather than discarding it via _ = db.Close().
  Pattern matches the existing rollback error wrap on line 102.

  InTx + InTxWithOptions panic-recovery defer: rollback failure now
  logs via slog.Error before re-panicking, rather than discarding
  the error via _ = tx.Rollback(). The original error context is
  preserved by the panic; the rollback failure is observable.

  jitteredDuration: switches from math/rand/v2 to crypto/rand +
  math/big. The function runs once per pool init at startup so the
  per-call cost (~microseconds) is irrelevant. Eliminates the gosec
  G404 trigger without a global exclude (handoff anti-relitigation:
  "don't add new excludes for one-off issues"). Adds the missing
  maxJitter <= 0 guard that the rand.Int64N call would have panicked
  on with a base smaller than 7ns.

middleware/ratelimit.go
  writeRateLimitExceeded: json.NewEncoder().Encode error is now
  logged via slog.Error rather than discarded. slog is already
  imported and used elsewhere in the file (line 60).

health/handler.go
  writeStatus: same pattern; adds log/slog import. Brings the file
  in line with the rest of the codebase's slog-everywhere discipline.

After: zero //nolint pragmas anywhere in backend Go code
(grep -rn "//nolint" returns nothing). golangci-lint run ./... reports
0 issues. All unit tests pass under -race; all integration tests pass
under -race -tags=integration with the testcontainer Postgres.

Surfaced by the pre-Phase-5 cross-phase alignment audit. Cleared as
part of finishing the Phase 0 lint-discipline alignment so Phases 5+
inherit a fully consistent codebase.
This commit is contained in:
CarterPerez-dev 2026-05-13 13:28:44 -04:00
parent fdc4144cbc
commit 13b2604f18
3 changed files with 40 additions and 13 deletions

View File

@ -5,9 +5,11 @@ package core
import (
"context"
crand "crypto/rand"
"database/sql"
"fmt"
"math/rand/v2"
"log/slog"
"math/big"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
@ -41,9 +43,15 @@ func NewDatabase(
pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := db.PingContext(pingCtx); err != nil {
_ = db.Close() //nolint:errcheck // cleanup on connection failure
return nil, fmt.Errorf("ping database: %w", err)
if pingErr := db.PingContext(pingCtx); pingErr != nil {
if closeErr := db.Close(); closeErr != nil {
return nil, fmt.Errorf(
"ping database: %w (close also failed: %w)",
pingErr,
closeErr,
)
}
return nil, fmt.Errorf("ping database: %w", pingErr)
}
return &Database{DB: db}, nil
@ -91,7 +99,12 @@ func InTx(ctx context.Context, db *sqlx.DB, fn func(tx *sqlx.Tx) error) error {
defer func() {
if p := recover(); p != nil {
_ = tx.Rollback() //nolint:errcheck // best-effort rollback on panic
if rbErr := tx.Rollback(); rbErr != nil {
slog.Error(
"rollback failed during panic recovery",
"error", rbErr,
)
}
panic(p)
}
}()
@ -123,7 +136,12 @@ func InTxWithOptions(
defer func() {
if p := recover(); p != nil {
_ = tx.Rollback() //nolint:errcheck // best-effort rollback on panic
if rbErr := tx.Rollback(); rbErr != nil {
slog.Error(
"rollback failed during panic recovery",
"error", rbErr,
)
}
panic(p)
}
}()
@ -143,7 +161,13 @@ func InTxWithOptions(
}
func jitteredDuration(base time.Duration) time.Duration {
//nolint:gosec // G404: non-security-sensitive jitter for connection pool
jitter := time.Duration(rand.Int64N(int64(base / 7)))
return base + jitter
maxJitter := int64(base / 7)
if maxJitter <= 0 {
return base
}
jitter, err := crand.Int(crand.Reader, big.NewInt(maxJitter))
if err != nil {
return base
}
return base + time.Duration(jitter.Int64())
}

View File

@ -6,6 +6,7 @@ package health
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"sync"
"sync/atomic"
@ -174,8 +175,9 @@ func (h *Handler) writeStatus(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.WriteHeader(status)
//nolint:errcheck // best-effort response
_ = json.NewEncoder(w).Encode(data)
if err := json.NewEncoder(w).Encode(data); err != nil {
slog.Error("failed to encode health response", "error", err)
}
}
type StatusResponse struct {

View File

@ -154,8 +154,9 @@ func writeRateLimitExceeded(w http.ResponseWriter, res *redis_rate.Result) {
},
}
//nolint:errcheck // best-effort response write
_ = json.NewEncoder(w).Encode(response)
if err := json.NewEncoder(w).Encode(response); err != nil {
slog.Error("failed to encode rate-limit response", "error", err)
}
}
type limiterEntry struct {