From 48c33ddba8695f54601c733686db7f6e76f4f6bd Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 14:26:53 -0400 Subject: [PATCH 01/10] refactor: simplify SecureToken API to panic on failure - Remove GenerateSecureTokenMust and SecureTokenMust variants - Change GenerateSecureToken and SecureToken to panic instead of returning errors - Remove arbitrary length cap (let users control token sizes) The Must variants are unnecessary because: - On Go 1.24+, crypto/rand.Read panics internally and never returns errors - On Go 1.23 and earlier, RNG failures indicate permanent system failures (uninitialized entropy, broken VM, etc.) that warrant panicking No upper length limit because: - Silent truncation would be a security footgun - Users should control their own token sizes - If a limit is needed, it should be enforced at the application level See: https://go.dev/issue/66821 --- common.go | 33 ++++++++++++--------------------- common_test.go | 41 ++++++++++++++--------------------------- 2 files changed, 26 insertions(+), 48 deletions(-) diff --git a/common.go b/common.go index 228ebab..abab3eb 100644 --- a/common.go +++ b/common.go @@ -102,39 +102,30 @@ func UUIDv4() string { // GenerateSecureToken generates a cryptographically secure random token encoded in base64. // It uses crypto/rand for randomness and base64.RawURLEncoding for URL-safe output. // If length is less than or equal to 0, it defaults to 32 bytes (256 bits of entropy). -// Returns an error if the random source fails. -func GenerateSecureToken(length int) (string, error) { +// Panics if the random source fails. +func GenerateSecureToken(length int) string { if length <= 0 { length = 32 } bytes := make([]byte, length) if _, err := randRead(bytes); err != nil { - return "", fmt.Errorf("utils: failed to read random bytes for token: %w", err) + // On Go 1.24+, crypto/rand.Read panics internally and never returns an error. + // On Go 1.23 and earlier, we panic for the same reasons: RNG failures indicate + // a broken system state (uninitialized entropy pool, misconfigured VM, etc.) + // that is almost certainly permanent rather than transient. + // See: https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/crypto/rand/rand.go + // https://go.dev/issue/66821 + panic(fmt.Sprintf("utils: failed to read random bytes for token: %v", err)) } - return base64.RawURLEncoding.EncodeToString(bytes), nil -} - -// GenerateSecureTokenMust is a convenience wrapper that panics on failure. -func GenerateSecureTokenMust(length int) string { - s, err := GenerateSecureToken(length) - if err != nil { - panic(err) - } - return s + return base64.RawURLEncoding.EncodeToString(bytes) } // SecureToken generates a secure token with 32 bytes of entropy. -// Returns an error if the random source fails. -func SecureToken() (string, error) { +// Panics if the random source fails. See GenerateSecureToken for details. +func SecureToken() string { return GenerateSecureToken(32) } -// SecureTokenMust generates a secure token with 32 bytes of entropy. -// It panics if the random source fails. -func SecureTokenMust() string { - return GenerateSecureTokenMust(32) -} - // FunctionName returns function name func FunctionName(fn any) string { if fn == nil { diff --git a/common_test.go b/common_test.go index 2adcf69..58039ea 100644 --- a/common_test.go +++ b/common_test.go @@ -103,32 +103,26 @@ func Test_UUIDv4_Concurrency(t *testing.T) { func Test_GenerateSecureToken(t *testing.T) { t.Parallel() // Test with 32 bytes - token, err := GenerateSecureToken(32) - require.NoError(t, err) + token := GenerateSecureToken(32) require.Len(t, token, 43) // base64 encoding of 32 bytes require.NotEmpty(t, token) // Test custom length - token8, err := GenerateSecureToken(8) - require.NoError(t, err) + token8 := GenerateSecureToken(8) require.Len(t, token8, 11) // base64 of 8 bytes ~11 chars - token16, err := GenerateSecureToken(16) - require.NoError(t, err) + token16 := GenerateSecureToken(16) require.Len(t, token16, 22) // base64 of 16 bytes ~22 chars // Test uniqueness - token2, err := GenerateSecureToken(32) - require.NoError(t, err) + token2 := GenerateSecureToken(32) require.NotEqual(t, token, token2) // Test invalid length defaults to 32 - tokenZero, err := GenerateSecureToken(0) - require.NoError(t, err) + tokenZero := GenerateSecureToken(0) require.Len(t, tokenZero, 43) - tokenNegative, err := GenerateSecureToken(-1) - require.NoError(t, err) + tokenNegative := GenerateSecureToken(-1) require.Len(t, tokenNegative, 43) } @@ -139,7 +133,7 @@ func Test_GenerateSecureToken_Concurrency(t *testing.T) { results := make(map[string]string) for i := 0; i < iterations; i++ { go func() { - ch <- GenerateSecureTokenMust(32) + ch <- GenerateSecureToken(32) }() } for i := 0; i < iterations; i++ { @@ -159,25 +153,18 @@ func Test_GenerateSecureToken_ErrorOnRandFail(t *testing.T) { return 0, errors.New("simulated failure") } - s, err := GenerateSecureToken(16) - require.Error(t, err) - require.Empty(t, s) - require.Contains(t, err.Error(), "simulated failure") - - // Must variant should panic on failure - require.Panics(t, func() { GenerateSecureTokenMust(16) }) + // Should panic on failure + require.Panics(t, func() { GenerateSecureToken(16) }) } func Test_SecureToken(t *testing.T) { t.Parallel() - token, err := SecureToken() - require.NoError(t, err) + token := SecureToken() require.Len(t, token, 43) require.NotEmpty(t, token) // Test uniqueness - token2, err := SecureToken() - require.NoError(t, err) + token2 := SecureToken() require.NotEqual(t, token, token2) } @@ -381,13 +368,13 @@ func Benchmark_GenerateSecureToken(b *testing.B) { var res string b.Run("16_bytes", func(b *testing.B) { for n := 0; n < b.N; n++ { - res = GenerateSecureTokenMust(16) + res = GenerateSecureToken(16) } require.Len(b, res, 22) }) b.Run("32_bytes", func(b *testing.B) { for n := 0; n < b.N; n++ { - res = GenerateSecureTokenMust(32) + res = GenerateSecureToken(32) } require.Len(b, res, 43) }) @@ -404,7 +391,7 @@ func Benchmark_TokenGenerators(b *testing.B) { }) b.Run("SecureToken", func(b *testing.B) { for n := 0; n < b.N; n++ { - res = SecureTokenMust() + res = SecureToken() } require.Len(b, res, 43) }) From 5d68de937086f1d903b7c90907413c513f8506ab Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 14:29:58 -0400 Subject: [PATCH 02/10] build: upgrade Go to 1.24.0 and update dependencies - Upgrade minimum Go version from 1.23.0 to 1.24.0 - Update golang.org/x/tools to v0.40.0 to fix compilation errors - This fixes the 'make test' error with tokeninternal.go --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index efd6981..edfb9c1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gofiber/utils/v2 -go 1.23.0 +go 1.24.0 require ( github.com/fxamacker/cbor/v2 v2.9.0 From 5c04dd3a0e35692719465a7903db6d94fe2801a7 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 14:31:18 -0400 Subject: [PATCH 03/10] build: update gotestsum to v1.13.0 - Upgrade gotestsum from v1.12.0 to v1.13.0 - This fixes compatibility with Go 1.24.0 and golang.org/x/tools v0.40.0 - Resolves the tokeninternal.go compilation error in 'make test' --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c25913c..5f0b50f 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ benchmark: ## coverage: ☂️ Generate coverage report .PHONY: coverage coverage: - go run gotest.tools/gotestsum@v1.12.0 -f testname -- ./... -race -count=1 -coverprofile=coverage.out -covermode=atomic + go run gotest.tools/gotestsum@v1.13.0 -f testname -- ./... -race -count=1 -coverprofile=coverage.out -covermode=atomic go tool cover -html=coverage.out -o coverage.html open coverage.html & @@ -46,7 +46,7 @@ modernize: ## test: 🚦 Execute all tests .PHONY: test test: - go run gotest.tools/gotestsum@v1.12.0 -f testname -- ./... -race -count=1 -shuffle=on + go run gotest.tools/gotestsum@v1.13.0 -f testname -- ./... -race -count=1 -shuffle=on ## tidy: 📌 Clean and tidy dependencies .PHONY: tidy From 4836c203fec5146f1d4dad43e18a5acd9e50f901 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 14:47:17 -0400 Subject: [PATCH 04/10] refactor: use fmt.Errorf instead of fmt.Sprintf for panic messages This follows Go best practices for error handling - using fmt.Errorf with %w allows recovery mechanisms to inspect error types and provides better error chaining if panics are recovered and inspected. --- common.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common.go b/common.go index abab3eb..886c5fd 100644 --- a/common.go +++ b/common.go @@ -57,7 +57,7 @@ func UUID() string { // Setup seed & counter once uuidSetup.Do(func() { if _, err := rand.Read(uuidSeed[:]); err != nil { - panic(fmt.Sprintf("utils: failed to seed UUID generator: %v", err)) + panic(fmt.Errorf("utils: failed to seed UUID generator: %w", err)) } uuidCounter = binary.LittleEndian.Uint64(uuidSeed[:8]) }) @@ -94,7 +94,7 @@ func UUID() string { func UUIDv4() string { token, err := uuid.NewRandom() if err != nil { - panic(fmt.Sprintf("utils: failed to generate secure UUID: %v", err)) + panic(fmt.Errorf("utils: failed to generate secure UUID: %w", err)) } return token.String() } @@ -115,7 +115,7 @@ func GenerateSecureToken(length int) string { // that is almost certainly permanent rather than transient. // See: https://cs.opensource.google/go/go/+/refs/tags/go1.24.0:src/crypto/rand/rand.go // https://go.dev/issue/66821 - panic(fmt.Sprintf("utils: failed to read random bytes for token: %v", err)) + panic(fmt.Errorf("utils: failed to read random bytes for token: %w", err)) } return base64.RawURLEncoding.EncodeToString(bytes) } From 209a657c80c7d28102aafc3ec2b966d85856f5ca Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 14:50:02 -0400 Subject: [PATCH 05/10] ci: update gotestsum to v1.13.0 for Go 1.24 compatibility --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3fd115..74f90c8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -31,7 +31,7 @@ jobs: go-version: ${{ matrix.go-version }} - name: Install gotestsum - run: go install gotest.tools/gotestsum@v1.12.0 + run: go install gotest.tools/gotestsum@v1.13.0 - name: Test run: gotestsum -f testname -- ./... -race -count=1 -shuffle=on From 08d4da789beaa3ad368dd7c01d609f87dc801bba Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 15:02:30 -0400 Subject: [PATCH 06/10] build: update Go version to 1.25.x in workflows and adjust coverage upload condition --- .github/workflows/benchmark.yml | 2 +- .github/workflows/linter.yml | 2 +- .github/workflows/test.yml | 6 +++--- go.mod | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 8be857b..28a8ae1 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -26,7 +26,7 @@ jobs: uses: actions/setup-go@v6 with: # NOTE: Keep this in sync with the version from go.mod - go-version: "1.23.x" + go-version: "1.25.x" - name: Run Benchmark run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 50a2c61..dff8b36 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/setup-go@v6 with: # NOTE: Keep this in sync with the version from go.mod - go-version: "1.23.x" + go-version: "1.25.x" cache: false - name: golangci-lint diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74f90c8..54c7647 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: Build: strategy: matrix: - go-version: [1.23.x, 1.24.x] + go-version: [1.23.x, 1.24.x, 1.25.x] platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] runs-on: ${{ matrix.platform }} steps: @@ -40,10 +40,10 @@ jobs: run: gotestsum -f testname -- ./... -race -count=1 -coverprofile=coverage.txt -covermode=atomic -shuffle=on - name: Upload coverage reports to Codecov - if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.24.x' }} + if: ${{ matrix.platform == 'ubuntu-latest' && matrix.go-version == '1.25.x' }} uses: codecov/codecov-action@v5.5.1 with: token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.txt + files: ./coverage.txt flags: unittests slug: gofiber/utils diff --git a/go.mod b/go.mod index edfb9c1..efd6981 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gofiber/utils/v2 -go 1.24.0 +go 1.23.0 require ( github.com/fxamacker/cbor/v2 v2.9.0 From a61d3b2577a60fbc83f3f3a143a6de2761dc7ecb Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 15:06:11 -0400 Subject: [PATCH 07/10] build: update Go version to 1.24.x in workflows and go.mod --- .github/workflows/test.yml | 2 +- go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 54c7647..a5f40a4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,7 +18,7 @@ jobs: Build: strategy: matrix: - go-version: [1.23.x, 1.24.x, 1.25.x] + go-version: [1.24.x, 1.25.x] platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] runs-on: ${{ matrix.platform }} steps: diff --git a/go.mod b/go.mod index efd6981..edfb9c1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gofiber/utils/v2 -go 1.23.0 +go 1.24.0 require ( github.com/fxamacker/cbor/v2 v2.9.0 From bdec7383b75a4994ba785b8ca934ddbfa4f566cc Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 15:14:41 -0400 Subject: [PATCH 08/10] build: update macOS version to 14 in CI workflow --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a5f40a4..da0bcb9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: go-version: [1.24.x, 1.25.x] - platform: [ubuntu-latest, windows-latest, macos-latest, macos-13] + platform: [ubuntu-latest, windows-latest, macos-latest, macos-14] runs-on: ${{ matrix.platform }} steps: - name: Fetch Repository From 4d5c057820beb67194c2da1b6a7a5a8e98cc5050 Mon Sep 17 00:00:00 2001 From: Jason McNeil Date: Tue, 9 Dec 2025 15:19:04 -0400 Subject: [PATCH 09/10] ci: revert changes out of PR scope --- common.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common.go b/common.go index 886c5fd..4083c79 100644 --- a/common.go +++ b/common.go @@ -57,7 +57,7 @@ func UUID() string { // Setup seed & counter once uuidSetup.Do(func() { if _, err := rand.Read(uuidSeed[:]); err != nil { - panic(fmt.Errorf("utils: failed to seed UUID generator: %w", err)) + panic(fmt.Sprintf("utils: failed to seed UUID generator: %v", err)) } uuidCounter = binary.LittleEndian.Uint64(uuidSeed[:8]) }) @@ -94,7 +94,7 @@ func UUID() string { func UUIDv4() string { token, err := uuid.NewRandom() if err != nil { - panic(fmt.Errorf("utils: failed to generate secure UUID: %w", err)) + panic(fmt.Sprintf("utils: failed to generate secure UUID: %v", err)) } return token.String() } From c5621356db0b5e13502ae16d7dd55f30b7d86996 Mon Sep 17 00:00:00 2001 From: RW Date: Fri, 12 Dec 2025 11:47:42 +0100 Subject: [PATCH 10/10] Apply suggestion from @ReneWerner87 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da0bcb9..7d6147d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: go-version: [1.24.x, 1.25.x] - platform: [ubuntu-latest, windows-latest, macos-latest, macos-14] + platform: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: - name: Fetch Repository