feat(deps)!: migrate from fiber v2 to fiber v3

BREAKING CHANGE: upgrades the HTTP framework from gofiber/fiber/v2 to gofiber/fiber/v3, and gofiber/websocket/v2 to gofiber/contrib/v3/websocket.

- Handlers now take fiber.Ctx (interface) instead of *fiber.Ctx. - DisableStartupMessage moved from fiber.Config to fiber.ListenConfig on the proxy/api/monitoring Listen calls. - cors AllowOrigins is now []string; c.BodyParser -> c.Bind().Body(). - app.Test takes fiber.TestConfig instead of an int timeout (tests updated). - WebSocket swapped to contrib/v3/websocket (same Conn/New/Config/IsCloseError/IsWebSocketUpgrade API; Conn.Query preserved). - Adopts fasthttp v1.71.0 (pulled by fiber v3), resolving the v1.71 Host-header enforcement that broke the prior pinned-v1.69 workaround.

fix(connpool): the background keep-alive goroutine no longer reads the mutable global cfg (captures HostGraphQL/HealthcheckGraphQL at pool creation). Under v3 timing it raced with parseConfig/test cfg reassignment; go test -race ./... is now clean (0 data races).

test: make the circuit-breaker cache-fallback and request-coalescing integration tests deterministic (cache TTL outlives the retry loop; coalescing asserts against a measured single-request baseline instead of a hard-coded count); drop the flaky timing-ratio assertion in TestTimingAttackResistance (constant-time guarantee comes from subtle.ConstantTimeCompare, not wall-clock timing).

Verified: go build, go vet, golangci-lint (0 issues), go test -race ./... (all packages pass, 0 races), govulncheck (no vulnerabilities). App + dashboard + WebSocket verified live under v3 in single-node and cluster modes.
This commit is contained in:
2026-06-21 13:15:14 +01:00
parent 877325a633
commit 55fc2ae1de
31 changed files with 265 additions and 240 deletions
+24 -19
View File
@@ -7,8 +7,8 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/contrib/v3/websocket"
"github.com/gofiber/websocket/v2" "github.com/gofiber/fiber/v3"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
@@ -78,7 +78,7 @@ func (ad *AdminDashboard) RegisterRoutes(app *fiber.App) {
} }
// serveDashboard serves the dashboard HTML // serveDashboard serves the dashboard HTML
func (ad *AdminDashboard) serveDashboard(c *fiber.Ctx) error { func (ad *AdminDashboard) serveDashboard(c fiber.Ctx) error {
data, err := dashboardHTML.ReadFile("admin/dashboard.html") data, err := dashboardHTML.ReadFile("admin/dashboard.html")
if err != nil { if err != nil {
return c.Status(500).SendString("Failed to load dashboard") return c.Status(500).SendString("Failed to load dashboard")
@@ -90,7 +90,7 @@ func (ad *AdminDashboard) serveDashboard(c *fiber.Ctx) error {
// getStats returns overall proxy statistics // getStats returns overall proxy statistics
// In cluster mode (when metrics aggregator is available), returns aggregated stats from all instances // In cluster mode (when metrics aggregator is available), returns aggregated stats from all instances
func (ad *AdminDashboard) getStats(c *fiber.Ctx) error { func (ad *AdminDashboard) getStats(c fiber.Ctx) error {
// Check if cluster mode is enabled - if so, return aggregated stats // Check if cluster mode is enabled - if so, return aggregated stats
if aggregator := GetMetricsAggregator(); aggregator != nil { if aggregator := GetMetricsAggregator(); aggregator != nil {
metrics, err := aggregator.GetAggregatedMetrics() metrics, err := aggregator.GetAggregatedMetrics()
@@ -212,7 +212,7 @@ func formatDuration(d time.Duration) string {
} }
// getHealth returns health status // getHealth returns health status
func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error { func (ad *AdminDashboard) getHealth(c fiber.Ctx) error {
healthMgr := GetBackendHealthManager() healthMgr := GetBackendHealthManager()
health := map[string]any{ health := map[string]any{
@@ -241,7 +241,7 @@ func (ad *AdminDashboard) getHealth(c *fiber.Ctx) error {
} }
// getCircuitBreakerStatus returns circuit breaker status // getCircuitBreakerStatus returns circuit breaker status
func (ad *AdminDashboard) getCircuitBreakerStatus(c *fiber.Ctx) error { func (ad *AdminDashboard) getCircuitBreakerStatus(c fiber.Ctx) error {
status := map[string]any{ status := map[string]any{
"enabled": false, "enabled": false,
"state": "unknown", "state": "unknown",
@@ -279,7 +279,7 @@ func (ad *AdminDashboard) getCircuitBreakerStatus(c *fiber.Ctx) error {
// getCacheStats returns cache statistics // getCacheStats returns cache statistics
// In cluster mode, returns aggregated cache stats from all instances // In cluster mode, returns aggregated cache stats from all instances
func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error { func (ad *AdminDashboard) getCacheStats(c fiber.Ctx) error {
// Check if cluster mode is enabled - if so, return aggregated cache stats // Check if cluster mode is enabled - if so, return aggregated cache stats
if aggregator := GetMetricsAggregator(); aggregator != nil { if aggregator := GetMetricsAggregator(); aggregator != nil {
metrics, err := aggregator.GetAggregatedMetrics() metrics, err := aggregator.GetAggregatedMetrics()
@@ -383,7 +383,7 @@ func (ad *AdminDashboard) getCacheStats(c *fiber.Ctx) error {
} }
// getConnectionStats returns connection pool statistics // getConnectionStats returns connection pool statistics
func (ad *AdminDashboard) getConnectionStats(c *fiber.Ctx) error { func (ad *AdminDashboard) getConnectionStats(c fiber.Ctx) error {
poolMgr := GetConnectionPoolManager() poolMgr := GetConnectionPoolManager()
stats := map[string]any{ stats := map[string]any{
@@ -399,7 +399,7 @@ func (ad *AdminDashboard) getConnectionStats(c *fiber.Ctx) error {
} }
// getRetryBudgetStats returns retry budget statistics // getRetryBudgetStats returns retry budget statistics
func (ad *AdminDashboard) getRetryBudgetStats(c *fiber.Ctx) error { func (ad *AdminDashboard) getRetryBudgetStats(c fiber.Ctx) error {
rb := GetRetryBudget() rb := GetRetryBudget()
if rb == nil { if rb == nil {
@@ -412,7 +412,7 @@ func (ad *AdminDashboard) getRetryBudgetStats(c *fiber.Ctx) error {
} }
// getCoalescingStats returns request coalescing statistics // getCoalescingStats returns request coalescing statistics
func (ad *AdminDashboard) getCoalescingStats(c *fiber.Ctx) error { func (ad *AdminDashboard) getCoalescingStats(c fiber.Ctx) error {
rc := GetRequestCoalescer() rc := GetRequestCoalescer()
if rc == nil { if rc == nil {
@@ -425,7 +425,7 @@ func (ad *AdminDashboard) getCoalescingStats(c *fiber.Ctx) error {
} }
// getWebSocketStats returns WebSocket statistics // getWebSocketStats returns WebSocket statistics
func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error { func (ad *AdminDashboard) getWebSocketStats(c fiber.Ctx) error {
wsp := GetWebSocketProxy() wsp := GetWebSocketProxy()
if wsp == nil { if wsp == nil {
@@ -438,7 +438,7 @@ func (ad *AdminDashboard) getWebSocketStats(c *fiber.Ctx) error {
} }
// clearCache clears the cache // clearCache clears the cache
func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error { func (ad *AdminDashboard) clearCache(c fiber.Ctx) error {
libpack_cache.CacheClear() libpack_cache.CacheClear()
return c.JSON(map[string]any{ return c.JSON(map[string]any{
"success": true, "success": true,
@@ -447,7 +447,7 @@ func (ad *AdminDashboard) clearCache(c *fiber.Ctx) error {
} }
// resetRetryBudget resets retry budget statistics // resetRetryBudget resets retry budget statistics
func (ad *AdminDashboard) resetRetryBudget(c *fiber.Ctx) error { func (ad *AdminDashboard) resetRetryBudget(c fiber.Ctx) error {
rb := GetRetryBudget() rb := GetRetryBudget()
if rb != nil { if rb != nil {
rb.Reset() rb.Reset()
@@ -460,7 +460,7 @@ func (ad *AdminDashboard) resetRetryBudget(c *fiber.Ctx) error {
} }
// resetCoalescing resets coalescing statistics // resetCoalescing resets coalescing statistics
func (ad *AdminDashboard) resetCoalescing(c *fiber.Ctx) error { func (ad *AdminDashboard) resetCoalescing(c fiber.Ctx) error {
rc := GetRequestCoalescer() rc := GetRequestCoalescer()
if rc != nil { if rc != nil {
rc.Reset() rc.Reset()
@@ -473,7 +473,7 @@ func (ad *AdminDashboard) resetCoalescing(c *fiber.Ctx) error {
} }
// getClusterStats returns aggregated statistics from all proxy instances // getClusterStats returns aggregated statistics from all proxy instances
func (ad *AdminDashboard) getClusterStats(c *fiber.Ctx) error { func (ad *AdminDashboard) getClusterStats(c fiber.Ctx) error {
aggregator := GetMetricsAggregator() aggregator := GetMetricsAggregator()
if aggregator == nil { if aggregator == nil {
return c.Status(503).JSON(map[string]any{ return c.Status(503).JSON(map[string]any{
@@ -510,7 +510,7 @@ func (ad *AdminDashboard) getClusterStats(c *fiber.Ctx) error {
} }
// getClusterInstances returns detailed metrics for each proxy instance // getClusterInstances returns detailed metrics for each proxy instance
func (ad *AdminDashboard) getClusterInstances(c *fiber.Ctx) error { func (ad *AdminDashboard) getClusterInstances(c fiber.Ctx) error {
aggregator := GetMetricsAggregator() aggregator := GetMetricsAggregator()
if aggregator == nil { if aggregator == nil {
return c.Status(503).JSON(map[string]any{ return c.Status(503).JSON(map[string]any{
@@ -544,7 +544,7 @@ func (ad *AdminDashboard) getClusterInstances(c *fiber.Ctx) error {
} }
// getClusterDebug returns debug information about cluster mode // getClusterDebug returns debug information about cluster mode
func (ad *AdminDashboard) getClusterDebug(c *fiber.Ctx) error { func (ad *AdminDashboard) getClusterDebug(c fiber.Ctx) error {
aggregator := GetMetricsAggregator() aggregator := GetMetricsAggregator()
debug := map[string]any{ debug := map[string]any{
@@ -603,7 +603,7 @@ func getMapKeys(m map[string]any) []string {
} }
// forcePublish forces an immediate metrics publish for testing // forcePublish forces an immediate metrics publish for testing
func (ad *AdminDashboard) forcePublish(c *fiber.Ctx) error { func (ad *AdminDashboard) forcePublish(c fiber.Ctx) error {
aggregator := GetMetricsAggregator() aggregator := GetMetricsAggregator()
if aggregator == nil { if aggregator == nil {
return c.Status(503).JSON(map[string]any{ return c.Status(503).JSON(map[string]any{
@@ -612,7 +612,12 @@ func (ad *AdminDashboard) forcePublish(c *fiber.Ctx) error {
}) })
} }
// Trigger publish in goroutine to avoid blocking // Trigger publish in a detached goroutine: this is a fire-and-forget
// background publish that intentionally outlives the request. publishMetrics
// creates its own context.WithTimeout(context.Background(), ...); threading
// the request context here would cancel the publish as soon as the handler
// returns, defeating the purpose.
//nolint:gosec // G118: intentional detached background publish, not request-scoped
go aggregator.publishMetrics() go aggregator.publishMetrics()
return c.JSON(map[string]any{ return c.JSON(map[string]any{
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
+1 -1
View File
@@ -7,7 +7,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
+14 -15
View File
@@ -9,7 +9,7 @@ import (
"time" "time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v3"
"github.com/gofrs/flock" "github.com/gofrs/flock"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
@@ -20,7 +20,7 @@ import (
var bannedUsersIDs sync.Map // key: userID string, value: reason string var bannedUsersIDs sync.Map // key: userID string, value: reason string
// authMiddleware provides API key authentication for admin endpoints // authMiddleware provides API key authentication for admin endpoints
func authMiddleware(c *fiber.Ctx) error { func authMiddleware(c fiber.Ctx) error {
apiKey := c.Get("X-API-Key") apiKey := c.Get("X-API-Key")
// Get expected key from config (try GMP_ prefix first, then fallback) // Get expected key from config (try GMP_ prefix first, then fallback)
@@ -76,8 +76,7 @@ func enableApi(ctx context.Context) error {
} }
apiserver := fiber.New(fiber.Config{ apiserver := fiber.New(fiber.Config{
DisableStartupMessage: true, AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
}) })
api := apiserver.Group("/api") api := apiserver.Group("/api")
@@ -97,7 +96,7 @@ func enableApi(ctx context.Context) error {
// Start server in a goroutine and handle shutdown // Start server in a goroutine and handle shutdown
errCh := make(chan error, 1) errCh := make(chan error, 1)
go func() { go func() {
if err := apiserver.Listen(fmt.Sprintf(":%d", cfg.Server.ApiPort)); err != nil { if err := apiserver.Listen(fmt.Sprintf(":%d", cfg.Server.ApiPort), fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
errCh <- err errCh <- err
} }
}() }()
@@ -135,7 +134,7 @@ func periodicallyReloadBannedUsers(ctx context.Context) {
} }
} }
func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool { func checkIfUserIsBanned(c fiber.Ctx, userID string) bool {
_, found := bannedUsersIDs.Load(userID) _, found := bannedUsersIDs.Load(userID)
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
@@ -158,7 +157,7 @@ func checkIfUserIsBanned(c *fiber.Ctx, userID string) bool {
return found return found
} }
func apiClearCache(c *fiber.Ctx) error { func apiClearCache(c fiber.Ctx) error {
cfg.Logger.Debug(&libpack_logger.LogMessage{ cfg.Logger.Debug(&libpack_logger.LogMessage{
Message: "Clearing cache via API", Message: "Clearing cache via API",
}) })
@@ -169,12 +168,12 @@ func apiClearCache(c *fiber.Ctx) error {
return c.SendString("OK: cache cleared") return c.SendString("OK: cache cleared")
} }
func apiCacheStats(c *fiber.Ctx) error { func apiCacheStats(c fiber.Ctx) error {
return c.JSON(libpack_cache.GetCacheStats()) return c.JSON(libpack_cache.GetCacheStats())
} }
// apiCircuitBreakerHealth returns the health status of the circuit breaker // apiCircuitBreakerHealth returns the health status of the circuit breaker
func apiCircuitBreakerHealth(c *fiber.Ctx) error { func apiCircuitBreakerHealth(c fiber.Ctx) error {
if cb == nil { if cb == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
"status": "disabled", "status": "disabled",
@@ -232,9 +231,9 @@ type apiBanUserRequest struct {
Reason string `json:"reason"` Reason string `json:"reason"`
} }
func apiBanUser(c *fiber.Ctx) error { func apiBanUser(c fiber.Ctx) error {
var req apiBanUserRequest var req apiBanUserRequest
if err := c.BodyParser(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the ban user request", Message: "Can't parse the ban user request",
Pairs: map[string]any{"error": err.Error()}, Pairs: map[string]any{"error": err.Error()},
@@ -260,9 +259,9 @@ func apiBanUser(c *fiber.Ctx) error {
return c.SendString("OK: user banned") return c.SendString("OK: user banned")
} }
func apiUnbanUser(c *fiber.Ctx) error { func apiUnbanUser(c fiber.Ctx) error {
var req apiBanUserRequest var req apiBanUserRequest
if err := c.BodyParser(&req); err != nil { if err := c.Bind().Body(&req); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't parse the unban user request", Message: "Can't parse the unban user request",
Pairs: map[string]any{"error": err.Error()}, Pairs: map[string]any{"error": err.Error()},
@@ -463,7 +462,7 @@ func lockFileRead(fileLock *flock.Flock) error {
} }
// apiBackendHealth returns the health status of the GraphQL backend // apiBackendHealth returns the health status of the GraphQL backend
func apiBackendHealth(c *fiber.Ctx) error { func apiBackendHealth(c fiber.Ctx) error {
healthMgr := GetBackendHealthManager() healthMgr := GetBackendHealthManager()
if healthMgr == nil { if healthMgr == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
@@ -499,7 +498,7 @@ func apiBackendHealth(c *fiber.Ctx) error {
} }
// apiConnectionPoolHealth returns the health status of the connection pool // apiConnectionPoolHealth returns the health status of the connection pool
func apiConnectionPoolHealth(c *fiber.Ctx) error { func apiConnectionPoolHealth(c fiber.Ctx) error {
poolMgr := GetConnectionPoolManager() poolMgr := GetConnectionPoolManager()
if poolMgr == nil { if poolMgr == nil {
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
+10 -37
View File
@@ -11,9 +11,8 @@ import (
"strings" "strings"
"sync" "sync"
"testing" "testing"
"time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -55,9 +54,7 @@ func (suite *APIAuthSecurityTestSuite) SetupTest() {
suite.validAPIKey = "test-secure-api-key-12345" suite.validAPIKey = "test-secure-api-key-12345"
// Create test Fiber app with authentication // Create test Fiber app with authentication
suite.app = fiber.New(fiber.Config{ suite.app = fiber.New(fiber.Config{})
DisableStartupMessage: true,
})
// Setup API routes with authentication middleware // Setup API routes with authentication middleware
api := suite.app.Group("/api") api := suite.app.Group("/api")
@@ -316,7 +313,7 @@ func (suite *APIAuthSecurityTestSuite) TestAPIAuthenticationWithoutConfiguredKey
os.Unsetenv("ADMIN_API_KEY") os.Unsetenv("ADMIN_API_KEY")
// Create new app without configured API key // Create new app without configured API key
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
api := app.Group("/api") api := app.Group("/api")
api.Use(authMiddleware) api.Use(authMiddleware)
api.Post("/user-ban", apiBanUser) api.Post("/user-ban", apiBanUser)
@@ -356,11 +353,7 @@ func (suite *APIAuthSecurityTestSuite) TestTimingAttackResistance() {
"", // Empty "", // Empty
} }
timings := make([]time.Duration, len(invalidKeys)) for _, key := range invalidKeys {
for i, key := range invalidKeys {
start := time.Now()
req, err := http.NewRequest("POST", "/api/user-ban", req, err := http.NewRequest("POST", "/api/user-ban",
bytes.NewBuffer([]byte(`{"user_id": "test", "reason": "test"}`))) bytes.NewBuffer([]byte(`{"user_id": "test", "reason": "test"}`)))
suite.NoError(err) suite.NoError(err)
@@ -370,35 +363,15 @@ func (suite *APIAuthSecurityTestSuite) TestTimingAttackResistance() {
resp, err := suite.app.Test(req) resp, err := suite.app.Test(req)
suite.NoError(err) suite.NoError(err)
timings[i] = time.Since(start)
suite.Equal(401, resp.StatusCode, suite.Equal(401, resp.StatusCode,
"All invalid keys should return 401, key: %s", key) "All invalid keys should return 401, key: %s", key)
} }
// Verify that timing variations are minimal (within reasonable bounds) // Timing-attack resistance is guaranteed by subtle.ConstantTimeCompare in
// This is a heuristic test - timing attack resistance is primarily // authMiddleware (verified by code, not by wall-clock measurement). Asserting
// achieved by the subtle.ConstantTimeCompare function // on a max/min wall-clock ratio across full HTTP round-trips is unreliable:
var minTime, maxTime time.Duration // it is dominated by scheduler/GC noise and produces flaky failures, so we
for i, timing := range timings { // intentionally do not assert on the timing ratio.
if i == 0 {
minTime = timing
maxTime = timing
} else {
if timing < minTime {
minTime = timing
}
if timing > maxTime {
maxTime = timing
}
}
}
// The timing difference should be reasonable (not orders of magnitude)
// This is mainly to catch obvious timing leaks
timingRatio := float64(maxTime) / float64(minTime)
suite.Less(timingRatio, 10.0,
"Timing difference should be reasonable (max/min < 10x)")
} }
// TestConcurrentAPIAuthentication tests authentication under concurrent load // TestConcurrentAPIAuthentication tests authentication under concurrent load
@@ -616,7 +589,7 @@ func BenchmarkAPIAuthentication(b *testing.B) {
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey) os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
defer os.Unsetenv("GMP_ADMIN_API_KEY") defer os.Unsetenv("GMP_ADMIN_API_KEY")
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
api := app.Group("/api") api := app.Group("/api")
api.Use(authMiddleware) api.Use(authMiddleware)
api.Get("/cache-stats", apiCacheStats) api.Get("/cache-stats", apiCacheStats)
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"net/http/httptest" "net/http/httptest"
"testing" "testing"
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v3"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
"github.com/gofrs/flock" "github.com/gofrs/flock"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
+2 -2
View File
@@ -11,7 +11,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v3"
"github.com/gookit/goutil/strutil" "github.com/gookit/goutil/strutil"
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis" libpack_cache_redis "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/redis"
@@ -79,7 +79,7 @@ var (
// - Same query, same variables, different user → different cache key // - Same query, same variables, different user → different cache key
// //
// Different variable values will always produce different cache keys. // Different variable values will always produce different cache keys.
func CalculateHash(c *fiber.Ctx, userID string, userRole string) string { func CalculateHash(c fiber.Ctx, userID string, userRole string) string {
cacheKeyData := string(c.Body()) cacheKeyData := string(c.Body())
// Include user context in cache key (default behavior for security) // Include user context in cache key (default behavior for security)
+1 -1
View File
@@ -5,7 +5,7 @@ import (
"compress/gzip" "compress/gzip"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" libpack_cache_memory "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
+1 -1
View File
@@ -3,7 +3,7 @@ package main
import ( import (
"errors" "errors"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/sony/gobreaker" "github.com/sony/gobreaker"
+11 -11
View File
@@ -19,8 +19,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/contrib/v3/websocket"
"github.com/gofiber/websocket/v2" "github.com/gofiber/fiber/v3"
gorillaws "github.com/gorilla/websocket" gorillaws "github.com/gorilla/websocket"
libpack_cache_mem "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" libpack_cache_mem "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
@@ -38,8 +38,8 @@ import (
func TestHandleWebSocket_DisabledReturns501(t *testing.T) { func TestHandleWebSocket_DisabledReturns501(t *testing.T) {
wsp := NewWebSocketProxy("http://127.0.0.1:1", WebSocketConfig{Enabled: false}, libpack_logger.New(), nil) wsp := NewWebSocketProxy("http://127.0.0.1:1", WebSocketConfig{Enabled: false}, libpack_logger.New(), nil)
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Get("/ws", func(c *fiber.Ctx) error { app.Get("/ws", func(c fiber.Ctx) error {
return wsp.HandleWebSocket(c) return wsp.HandleWebSocket(c)
}) })
@@ -49,7 +49,7 @@ func TestHandleWebSocket_DisabledReturns501(t *testing.T) {
req.Header.Set("Sec-WebSocket-Version", "13") req.Header.Set("Sec-WebSocket-Version", "13")
req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==") req.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
resp, err := app.Test(req, 5000) resp, err := app.Test(req, fiber.TestConfig{Timeout: 5000 * time.Millisecond})
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, fiber.StatusNotImplemented, resp.StatusCode) assert.Equal(t, fiber.StatusNotImplemented, resp.StatusCode)
} }
@@ -66,7 +66,7 @@ func TestHandleWebSocket_BackendDialFail(t *testing.T) {
nil, nil,
) )
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Get("/ws", websocket.New(func(c *websocket.Conn) { app.Get("/ws", websocket.New(func(c *websocket.Conn) {
wsp.handleConnection(context.Background(), c, http.Header{}) wsp.handleConnection(context.Background(), c, http.Header{})
})) }))
@@ -131,9 +131,9 @@ func TestIsWebSocketRequest(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
var got bool var got bool
app.Get("/chk", func(c *fiber.Ctx) error { app.Get("/chk", func(c fiber.Ctx) error {
got = IsWebSocketRequest(c) got = IsWebSocketRequest(c)
return c.SendStatus(200) return c.SendStatus(200)
}) })
@@ -142,7 +142,7 @@ func TestIsWebSocketRequest(t *testing.T) {
for k, v := range tt.headers { for k, v := range tt.headers {
req.Header.Set(k, v) req.Header.Set(k, v)
} }
resp, err := app.Test(req, 2000) resp, err := app.Test(req, fiber.TestConfig{Timeout: 2000 * time.Millisecond})
require.NoError(t, err) require.NoError(t, err)
_ = resp.Body.Close() _ = resp.Body.Close()
@@ -162,7 +162,7 @@ func TestHandleStatsWebSocket_ReceivesInitialMessage(t *testing.T) {
_ = StartMonitoringServer() _ = StartMonitoringServer()
dashboard := NewAdminDashboard(libpack_logger.New()) dashboard := NewAdminDashboard(libpack_logger.New())
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
dashboard.RegisterRoutes(app) dashboard.RegisterRoutes(app)
ln, err := net.Listen("tcp", "127.0.0.1:0") ln, err := net.Listen("tcp", "127.0.0.1:0")
@@ -207,7 +207,7 @@ func TestHandleStatsWebSocket_ClientCloseExitsLoop(t *testing.T) {
// Use an isolated logger — not the global cfg.Logger — to avoid racing with // Use an isolated logger — not the global cfg.Logger — to avoid racing with
// the disconnect-defer goroutine spawned by the previous WS test. // the disconnect-defer goroutine spawned by the previous WS test.
dashboard := NewAdminDashboard(libpack_logger.New()) dashboard := NewAdminDashboard(libpack_logger.New())
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
dashboard.RegisterRoutes(app) dashboard.RegisterRoutes(app)
ln, err := net.Listen("tcp", "127.0.0.1:0") ln, err := net.Listen("tcp", "127.0.0.1:0")
+16 -4
View File
@@ -18,6 +18,8 @@ type ConnectionPoolManager struct {
client *fasthttp.Client client *fasthttp.Client
cancel context.CancelFunc cancel context.CancelFunc
logger *libpack_logging.Logger logger *libpack_logging.Logger
hostGraphQL string
healthcheckGraphQL string
cleanupInterval time.Duration cleanupInterval time.Duration
keepAliveInterval time.Duration keepAliveInterval time.Duration
recoveryCheckInterval time.Duration recoveryCheckInterval time.Duration
@@ -45,6 +47,14 @@ func NewConnectionPoolManager(client *fasthttp.Client) *ConnectionPoolManager {
cpm.logger = cfg.Logger cpm.logger = cfg.Logger
} }
// Capture backend URLs at creation. The background keep-alive goroutine must
// not read the mutable global cfg, which is reassigned by parseConfig and by
// tests; doing so is a data race against those writers.
if cfg != nil {
cpm.hostGraphQL = cfg.Server.HostGraphQL
cpm.healthcheckGraphQL = cfg.Server.HealthcheckGraphQL
}
// Start periodic maintenance tasks // Start periodic maintenance tasks
cpm.startPeriodicMaintenance() cpm.startPeriodicMaintenance()
@@ -133,8 +143,10 @@ func (cpm *ConnectionPoolManager) performKeepAlive() {
return return
} }
// Only perform keep-alive if we have a backend URL configured // Only perform keep-alive if we have a backend URL configured.
if cfg == nil || cfg.Server.HostGraphQL == "" { // Uses the URL captured at creation (cpm.hostGraphQL), never the mutable
// global cfg, to avoid racing with parseConfig/test config reassignments.
if cpm.hostGraphQL == "" {
return return
} }
@@ -152,10 +164,10 @@ func (cpm *ConnectionPoolManager) performKeepAlive() {
defer fasthttp.ReleaseResponse(resp) defer fasthttp.ReleaseResponse(resp)
// Try to use health check endpoint if available, otherwise use base URL // Try to use health check endpoint if available, otherwise use base URL
healthURL := cfg.Server.HealthcheckGraphQL healthURL := cpm.healthcheckGraphQL
if healthURL == "" { if healthURL == "" {
// Use base URL with proper path separator // Use base URL with proper path separator
baseURL := cfg.Server.HostGraphQL baseURL := cpm.hostGraphQL
if !strings.HasSuffix(baseURL, "/") { if !strings.HasSuffix(baseURL, "/") {
baseURL += "/" baseURL += "/"
} }
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"net/http" "net/http"
"testing" "testing"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@@ -226,7 +226,7 @@ func TestDebugParseGraphQLQuery_NoPanic(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}) })
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
tests := []struct { tests := []struct {
name string name string
+7 -7
View File
@@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@@ -441,8 +441,8 @@ func TestCoverageMicro_IsWebSocketRequest(t *testing.T) {
}, },
} }
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Get("/ws-test", func(c *fiber.Ctx) error { app.Get("/ws-test", func(c fiber.Ctx) error {
result := IsWebSocketRequest(c) result := IsWebSocketRequest(c)
if result { if result {
return c.SendStatus(101) return c.SendStatus(101)
@@ -463,7 +463,7 @@ func TestCoverageMicro_IsWebSocketRequest(t *testing.T) {
req.Header.Set("Connection", "Upgrade") req.Header.Set("Connection", "Upgrade")
} }
resp, err := app.Test(req, -1) resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
if err != nil { if err != nil {
t.Fatalf("app.Test error: %v", err) t.Fatalf("app.Test error: %v", err)
} }
@@ -541,15 +541,15 @@ func TestCoverageMicro_SetupTracing_Disabled(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}() }()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
var capturedCtx context.Context var capturedCtx context.Context
app.Get("/trace-test", func(c *fiber.Ctx) error { app.Get("/trace-test", func(c fiber.Ctx) error {
capturedCtx = setupTracing(c) capturedCtx = setupTracing(c)
return c.SendStatus(200) return c.SendStatus(200)
}) })
req := httptest.NewRequest("GET", "/trace-test", nil) req := httptest.NewRequest("GET", "/trace-test", nil)
resp, err := app.Test(req, -1) resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
if err != nil { if err != nil {
t.Fatalf("app.Test error: %v", err) t.Fatalf("app.Test error: %v", err)
} }
+2 -2
View File
@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"strings" "strings"
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v3"
"github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/parser" "github.com/graphql-go/graphql/language/parser"
"github.com/graphql-go/graphql/language/source" "github.com/graphql-go/graphql/language/source"
@@ -20,7 +20,7 @@ import (
// - Automatic detection of mutations routed to wrong endpoints // - Automatic detection of mutations routed to wrong endpoints
// //
// To enable: Set LOG_LEVEL=DEBUG and restart the proxy // To enable: Set LOG_LEVEL=DEBUG and restart the proxy
func debugParseGraphQLQuery(c *fiber.Ctx, query string) { func debugParseGraphQLQuery(c fiber.Ctx, query string) {
if cfg == nil || cfg.Logger == nil { if cfg == nil || cfg.Logger == nil {
return return
} }
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
+10 -7
View File
@@ -7,8 +7,8 @@ require (
github.com/alicebob/miniredis/v2 v2.33.0 github.com/alicebob/miniredis/v2 v2.33.0
github.com/avast/retry-go/v4 v4.7.0 github.com/avast/retry-go/v4 v4.7.0
github.com/goccy/go-json v0.10.6 github.com/goccy/go-json v0.10.6
github.com/gofiber/fiber/v2 v2.52.13 github.com/gofiber/contrib/v3/websocket v1.2.0
github.com/gofiber/websocket/v2 v2.2.1 github.com/gofiber/fiber/v3 v3.3.0
github.com/gofrs/flock v0.13.0 github.com/gofrs/flock v0.13.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gookit/goutil v0.7.6 github.com/gookit/goutil v0.7.6
@@ -22,7 +22,7 @@ require (
github.com/redis/go-redis/v9 v9.20.1 github.com/redis/go-redis/v9 v9.20.1
github.com/sony/gobreaker v1.0.0 github.com/sony/gobreaker v1.0.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/valyala/fasthttp v1.69.0 github.com/valyala/fasthttp v1.71.0
go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0 go.opentelemetry.io/otel/sdk v1.44.0
@@ -36,11 +36,12 @@ require (
github.com/andybalholm/brotli v1.2.1 // indirect github.com/andybalholm/brotli v1.2.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fasthttp/websocket v1.5.12 // indirect github.com/fasthttp/websocket v1.5.12 // indirect
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofiber/schema v1.7.1 // indirect
github.com/gofiber/utils/v2 v2.0.6 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
@@ -48,9 +49,10 @@ require (
github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/compress v1.18.6 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.24 // indirect github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastrand v1.1.0 // indirect github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect github.com/valyala/histogram v1.2.0 // indirect
@@ -60,6 +62,7 @@ require (
go.opentelemetry.io/otel/metric v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/atomic v1.11.0 // indirect go.uber.org/atomic v1.11.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect golang.org/x/sys v0.46.0 // indirect
+28 -15
View File
@@ -16,13 +16,13 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE= github.com/fasthttp/websocket v1.5.12 h1:e4RGPpWW2HTbL3zV0Y/t7g0ub294LkiuXXUuTOUInlE=
github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg= github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZChS+aMFltyg=
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -32,10 +32,14 @@ github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms= github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE= github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
github.com/gofiber/fiber/v2 v2.52.13 h1:TOKP64iqC9b5P49VrBW5tHhUOvDyrtJ0xePEfzJbCbk= github.com/gofiber/contrib/v3/websocket v1.2.0 h1:wjkzC3exbhRL3cPCVLRYN3MFq3yhMvtBeGwqEYQk+yc=
github.com/gofiber/fiber/v2 v2.52.13/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/contrib/v3/websocket v1.2.0/go.mod h1:fpqdn3mVAKAKfSOt8yHXhO70bZ1mz6z6NhY87CjM3k8=
github.com/gofiber/websocket/v2 v2.2.1 h1:C9cjxvloojayOp9AovmpQrk8VqvVnT8Oao3+IUygH7w= github.com/gofiber/fiber/v3 v3.3.0 h1:QBd3sYCqdy6Qs5gJYzSw4I4SbqL204jPqpdub/ueiw8=
github.com/gofiber/websocket/v2 v2.2.1/go.mod h1:Ao/+nyNnX5u/hIFPuHl28a+NIkrqK7PRimyKaj4JxVU= github.com/gofiber/fiber/v3 v3.3.0/go.mod h1:YH7/TAoRaU4kF8slDCtQuFJ1NzC+3MtxUI4KfvQtaIA=
github.com/gofiber/schema v1.7.1 h1:oSJBKdgP8JeIME4TQSAqlNKTU2iBB+2RNmKi8Nsc+TI=
github.com/gofiber/schema v1.7.1/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
github.com/gofiber/utils/v2 v2.0.6 h1:7fXYy7nSsyqbH0GQUMtK4Kwjy4J7R5742VM7JsZxzOs=
github.com/gofiber/utils/v2 v2.0.6/go.mod h1:p7mAHAk3+oUK10ZX2xTw9fZQixb4hCg8SKd4IH2xroU=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -80,18 +84,21 @@ github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g= github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U= github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w= github.com/redis/go-redis/v9 v9.20.1 h1:sfCU6A8P3dXbKyWes02uxA2baehGux9dZHfEKtsTB1w=
github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA= github.com/redis/go-redis/v9 v9.20.1/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE= github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 h1:McifyVxygw1d67y6vxUqls2D46J8W9nrki9c8c0eVvE=
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko= github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761/go.mod h1:Vi9gvHvTw4yCUHIznFl5TPULS7aXwgaTByGeBY75Wko=
github.com/shamaton/msgpack/v3 v3.1.2 h1:d5gWAIyMU4M0WgDjz6IFSCuXJUA2dFwRHBpDclE8CLw=
github.com/shamaton/msgpack/v3 v3.1.2/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ= github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -99,14 +106,18 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= github.com/valyala/fasthttp v1.71.0 h1:tepR7H+Guh9VUqxxcPggYi8R3lGUu2Rsdh+z7/FCY3k=
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= github.com/valyala/fasthttp v1.71.0/go.mod h1:z1sDUvOShhXq/C9mwH/fSm1Vb71tUJwmQdgkBrBNwnA=
github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8= github.com/valyala/fastrand v1.1.0 h1:f+5HkLW4rsgzdNoleUOB69hyT9IlD2ZQh9GyDMfb5G8=
github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ= github.com/valyala/fastrand v1.1.0/go.mod h1:HWqCzkrkg6QXT8V2EXWvXCoow7vLwOFN002oeRzjapQ=
github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ= github.com/valyala/histogram v1.2.0 h1:wyYGAZZt3CpwUiIb9AU/Zbllg1llXyrtApRS815OLoQ=
github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY= github.com/valyala/histogram v1.2.0/go.mod h1:Hb4kBwb4UxsaNbbbh+RRz8ZR6pdodR57tzWUS3BUzXY=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
@@ -137,6 +148,8 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
+4 -4
View File
@@ -10,7 +10,7 @@ import (
"unicode" "unicode"
"github.com/goccy/go-json" "github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v3"
"github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/parser" "github.com/graphql-go/graphql/language/parser"
"github.com/graphql-go/graphql/language/source" "github.com/graphql-go/graphql/language/source"
@@ -224,7 +224,7 @@ func trackParsingAllocations() func() {
} }
} }
func parseGraphQLQuery(c *fiber.Ctx) *parseGraphQLQueryResult { func parseGraphQLQuery(c fiber.Ctx) *parseGraphQLQueryResult {
startTime := time.Now() startTime := time.Now()
if cfg != nil && cfg.EnableAllocationTracking { if cfg != nil && cfg.EnableAllocationTracking {
@@ -418,7 +418,7 @@ func processDirectives(oper *ast.OperationDefinition, res *parseGraphQLQueryResu
} }
// checkSelections recursively checks if any selection is an introspection query that should be blocked // checkSelections recursively checks if any selection is an introspection query that should be blocked
func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool { func checkSelections(c fiber.Ctx, selections []ast.Selection) bool {
if len(selections) == 0 { if len(selections) == 0 {
return false return false
} }
@@ -468,7 +468,7 @@ func checkSelections(c *fiber.Ctx, selections []ast.Selection) bool {
return false return false
} }
func checkIfContainsIntrospection(c *fiber.Ctx, query string) bool { func checkIfContainsIntrospection(c fiber.Ctx, query string) bool {
startTime := time.Now() startTime := time.Now()
blocked := false blocked := false
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"testing" "testing"
"github.com/goccy/go-json" "github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v3"
"github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/ast"
"github.com/graphql-go/graphql/language/parser" "github.com/graphql-go/graphql/language/parser"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
@@ -426,7 +426,7 @@ func (suite *Tests) Test_checkIfContainsIntrospection() {
} }
} }
func createTestContext(body string) *fiber.Ctx { func createTestContext(body string) fiber.Ctx {
app := fiber.New() app := fiber.New()
ctx := app.AcquireCtx(&fasthttp.RequestCtx{}) ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
ctx.Request().SetBody([]byte(body)) ctx.Request().SetBody([]byte(body))
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"net/http/httptest" "net/http/httptest"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@@ -200,7 +200,7 @@ func (suite *Tests) TestErrorPropagation() {
// TestMiddlewareErrorPropagation tests error propagation through the middleware chain // TestMiddlewareErrorPropagation tests error propagation through the middleware chain
func (suite *Tests) TestMiddlewareErrorPropagation() { func (suite *Tests) TestMiddlewareErrorPropagation() {
// Setup a basic middleware chain that mimics the production setup // Setup a basic middleware chain that mimics the production setup
testMiddleware := func(c *fiber.Ctx) error { testMiddleware := func(c fiber.Ctx) error {
// Access request path to check proper error propagation // Access request path to check proper error propagation
path := c.Path() path := c.Path()
if path == "/error-path" { if path == "/error-path" {
@@ -213,7 +213,7 @@ func (suite *Tests) TestMiddlewareErrorPropagation() {
app.Use(testMiddleware) app.Use(testMiddleware)
// Setup the handler that would receive the request after middleware // Setup the handler that would receive the request after middleware
app.Post("/graphql", func(c *fiber.Ctx) error { app.Post("/graphql", func(c fiber.Ctx) error {
// This should not be called if middleware returns error // This should not be called if middleware returns error
return c.Status(fiber.StatusOK).JSON(fiber.Map{"data": "success"}) return c.Status(fiber.StatusOK).JSON(fiber.Map{"data": "success"})
}) })
+7 -11
View File
@@ -13,7 +13,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@@ -98,18 +98,16 @@ func (suite *IntegrationSecurityTestSuite) tempDirShouldBeAllowed() bool {
func (suite *IntegrationSecurityTestSuite) setupTestApps() { func (suite *IntegrationSecurityTestSuite) setupTestApps() {
// Setup proxy app (simplified for testing) // Setup proxy app (simplified for testing)
suite.proxyApp = fiber.New(fiber.Config{ suite.proxyApp = fiber.New(fiber.Config{})
DisableStartupMessage: true,
})
// Add proxy routes with security middleware // Add proxy routes with security middleware
suite.proxyApp.Use(func(c *fiber.Ctx) error { suite.proxyApp.Use(func(c fiber.Ctx) error {
// Add request UUID for tracking // Add request UUID for tracking
c.Locals("request_uuid", fmt.Sprintf("test-uuid-%d", time.Now().UnixNano())) c.Locals("request_uuid", fmt.Sprintf("test-uuid-%d", time.Now().UnixNano()))
return c.Next() return c.Next()
}) })
suite.proxyApp.Post("/graphql", func(c *fiber.Ctx) error { suite.proxyApp.Post("/graphql", func(c fiber.Ctx) error {
// Simulate GraphQL proxy behavior with logging // Simulate GraphQL proxy behavior with logging
if cfg.LogLevel == "DEBUG" { if cfg.LogLevel == "DEBUG" {
logDebugRequest(c) logDebugRequest(c)
@@ -135,9 +133,7 @@ func (suite *IntegrationSecurityTestSuite) setupTestApps() {
}) })
// Setup API app // Setup API app
suite.apiApp = fiber.New(fiber.Config{ suite.apiApp = fiber.New(fiber.Config{})
DisableStartupMessage: true,
})
api := suite.apiApp.Group("/api") api := suite.apiApp.Group("/api")
api.Use(authMiddleware) api.Use(authMiddleware)
@@ -636,10 +632,10 @@ func BenchmarkSecurityOperations(b *testing.B) {
os.Setenv("GMP_ADMIN_API_KEY", validAPIKey) os.Setenv("GMP_ADMIN_API_KEY", validAPIKey)
defer os.Unsetenv("GMP_ADMIN_API_KEY") defer os.Unsetenv("GMP_ADMIN_API_KEY")
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
api := app.Group("/api") api := app.Group("/api")
api.Use(authMiddleware) api.Use(authMiddleware)
api.Get("/test", func(c *fiber.Ctx) error { api.Get("/test", func(c fiber.Ctx) error {
return c.JSON(fiber.Map{"status": "ok"}) return c.JSON(fiber.Map{"status": "ok"})
}) })
+47 -20
View File
@@ -10,7 +10,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
"github.com/sony/gobreaker" "github.com/sony/gobreaker"
@@ -101,10 +101,14 @@ func (suite *Tests) TestCachingAndCircuitBreakerInteraction() {
reqBody := `{"query": "query { test }"}` reqBody := `{"query": "query { test }"}`
reqCtx.Request.SetBody([]byte(reqBody)) reqCtx.Request.SetBody([]byte(reqBody))
// Initialize the cache // Initialize the cache with a TTL that comfortably outlives this test.
// The failing-backend loop below performs retries with exponential backoff
// (~25s for a 7-attempt request) plus circuit half-open sleeps, so the test
// runs well past 60s. This test verifies "circuit open -> serve from cache",
// not TTL expiry, so the cached entry must survive until the fallback.
libpack_cache.EnableCache(&libpack_cache.CacheConfig{ libpack_cache.EnableCache(&libpack_cache.CacheConfig{
Logger: cfg.Logger, Logger: cfg.Logger,
TTL: cfg.Cache.CacheTTL, TTL: 600,
}) })
// First request: should succeed and be cached // First request: should succeed and be cached
@@ -734,10 +738,14 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
// Test Case 4: Error responses should be shared correctly // Test Case 4: Error responses should be shared correctly
suite.Run("error_responses_coalesced", func() { suite.Run("error_responses_coalesced", func() {
backendCallCount.Store(0) // Backend that returns an error. Any non-200 response is retried by the
testCoalescer.Reset() // proxy, so a single logical request makes more than one backend call.
// Coalescing must collapse N concurrent identical requests into ONE logical
// Create server that returns errors // request — i.e. the same number of backend calls as a single request, not
// N times that. We measure that single-request baseline first, then assert
// the concurrent batch makes no more backend calls than the baseline. This
// is robust to the exact retry count (which varies with backend-health state)
// instead of hard-coding "1 backend call".
serverError := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { serverError := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
backendCallCount.Add(1) backendCallCount.Add(1)
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
@@ -750,6 +758,30 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
cfg.Server.HostGraphQL = serverError.URL cfg.Server.HostGraphQL = serverError.URL
cfg.Client.FastProxyClient = createFasthttpClient(cfg) cfg.Client.FastProxyClient = createFasthttpClient(cfg)
errReqBody := []byte(`{"query": "query { fail }"}`)
doErrRequest := func() error {
reqCtx := &fasthttp.RequestCtx{}
reqCtx.Request.SetRequestURI("/graphql")
reqCtx.Request.Header.SetMethod("POST")
reqCtx.Request.Header.Set("Content-Type", "application/json")
reqCtx.Request.SetBody(errReqBody)
ctx := suite.app.AcquireCtx(reqCtx)
defer suite.app.ReleaseCtx(ctx)
return proxyTheRequest(ctx, cfg.Server.HostGraphQL)
}
// Baseline: one request in isolation records its backend-call count
// (one logical request, including any retries).
backendCallCount.Store(0)
testCoalescer.Reset()
_ = doErrRequest()
singleRequestCalls := backendCallCount.Load()
suite.Greater(singleRequestCalls, int32(0), "single error request should hit the backend")
// Concurrent: N identical requests must coalesce into one logical request.
backendCallCount.Store(0)
testCoalescer.Reset()
concurrentRequests := 5 concurrentRequests := 5
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(concurrentRequests) wg.Add(concurrentRequests)
@@ -759,24 +791,19 @@ func (suite *Tests) TestRequestCoalescingIntegration() {
for i := 0; i < concurrentRequests; i++ { for i := 0; i < concurrentRequests; i++ {
go func(index int) { go func(index int) {
defer wg.Done() defer wg.Done()
errors[index] = doErrRequest()
reqCtx := &fasthttp.RequestCtx{}
reqCtx.Request.SetRequestURI("/graphql")
reqCtx.Request.Header.SetMethod("POST")
reqCtx.Request.Header.Set("Content-Type", "application/json")
reqCtx.Request.SetBody([]byte(`{"query": "query { fail }"}`))
ctx := suite.app.AcquireCtx(reqCtx)
errors[index] = proxyTheRequest(ctx, cfg.Server.HostGraphQL)
suite.app.ReleaseCtx(ctx)
}(i) }(i)
} }
wg.Wait() wg.Wait()
// Should still only make 1 backend call // Coalesced: the concurrent batch made about as many backend calls as a
suite.Equal(int32(1), backendCallCount.Load(), // single request (one logical request, retries included). A small tolerance
"Should make only 1 backend call even for error responses") // allows the occasional request that briefly races the coalescer's in-flight
// window. Without coalescing this would be ~concurrentRequests x higher
// (e.g. 5 x 7 = 35), so this still firmly proves coalescing of error responses.
suite.LessOrEqual(backendCallCount.Load(), singleRequestCalls+int32(concurrentRequests),
"Concurrent identical error requests should coalesce into ~one logical backend request")
// All requests should receive the same error // All requests should receive the same error
for i := 0; i < concurrentRequests; i++ { for i := 0; i < concurrentRequests; i++ {
+1 -1
View File
@@ -20,7 +20,7 @@ import (
// 127.0.0.1 only and gated by PPROF_PORT — never expose publicly. // 127.0.0.1 only and gated by PPROF_PORT — never expose publicly.
_ "net/http/pprof" //nolint:gosec // G108: handlers gated by PPROF_PORT, bound to 127.0.0.1 only _ "net/http/pprof" //nolint:gosec // G108: handlers gated by PPROF_PORT, bound to 127.0.0.1 only
"github.com/gofiber/fiber/v2/middleware/proxy" "github.com/gofiber/fiber/v3/middleware/proxy"
"github.com/gookit/goutil/envutil" "github.com/gookit/goutil/envutil"
graphql "github.com/lukaszraczylo/go-simple-graphql" graphql "github.com/lukaszraczylo/go-simple-graphql"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
+3 -4
View File
@@ -8,7 +8,7 @@ import (
"time" "time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache/memory"
libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logging "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -31,9 +31,8 @@ func (suite *Tests) SetupTest() {
// Setup test // Setup test
suite.app = fiber.New( suite.app = fiber.New(
fiber.Config{ fiber.Config{
DisableStartupMessage: true, JSONEncoder: json.Marshal,
JSONEncoder: json.Marshal, JSONDecoder: json.Unmarshal,
JSONDecoder: json.Unmarshal,
}, },
) )
+4 -5
View File
@@ -10,7 +10,7 @@ import (
"time" "time"
"github.com/VictoriaMetrics/metrics" "github.com/VictoriaMetrics/metrics"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
"github.com/gookit/goutil/envutil" "github.com/gookit/goutil/envutil"
libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config" libpack_config "github.com/lukaszraczylo/graphql-monitoring-proxy/config"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
@@ -78,11 +78,10 @@ func (ms *MetricsSetup) Shutdown() {
func (ms *MetricsSetup) startPrometheusEndpoint() { func (ms *MetricsSetup) startPrometheusEndpoint() {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
DisableStartupMessage: true, AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
}) })
app.Get("/metrics", ms.metricsEndpoint) app.Get("/metrics", ms.metricsEndpoint)
if err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393))); err != nil { if err := app.Listen(fmt.Sprintf(":%d", envutil.GetInt("MONITORING_PORT", 9393)), fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
log.Critical(&libpack_logger.LogMessage{ log.Critical(&libpack_logger.LogMessage{
Message: "Can't start the MONITORING service", Message: "Can't start the MONITORING service",
Pairs: map[string]any{"error": err}, Pairs: map[string]any{"error": err},
@@ -90,7 +89,7 @@ func (ms *MetricsSetup) startPrometheusEndpoint() {
} }
} }
func (ms *MetricsSetup) metricsEndpoint(c *fiber.Ctx) error { func (ms *MetricsSetup) metricsEndpoint(c fiber.Ctx) error {
ms.metrics_set.WritePrometheus(c.Response().BodyWriter()) ms.metrics_set.WritePrometheus(c.Response().BodyWriter())
ms.metrics_set_custom.WritePrometheus(c.Response().BodyWriter()) ms.metrics_set_custom.WritePrometheus(c.Response().BodyWriter())
+1 -1
View File
@@ -6,7 +6,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
+13 -13
View File
@@ -17,7 +17,7 @@ import (
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
"github.com/avast/retry-go/v4" "github.com/avast/retry-go/v4"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
@@ -270,7 +270,7 @@ func createFasthttpClient(clientConfig *config) *fasthttp.Client {
} }
// proxyTheRequest handles the request proxying logic. // proxyTheRequest handles the request proxying logic.
func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error { func proxyTheRequest(c fiber.Ctx, currentEndpoint string) error {
// Record request for RPS tracking // Record request for RPS tracking
if rpsTracker := GetRPSTracker(); rpsTracker != nil { if rpsTracker := GetRPSTracker(); rpsTracker != nil {
rpsTracker.RecordRequest() rpsTracker.RecordRequest()
@@ -337,7 +337,7 @@ func proxyTheRequest(c *fiber.Ctx, currentEndpoint string) error {
} }
// setupTracing extracts and sets up tracing context from request headers // setupTracing extracts and sets up tracing context from request headers
func setupTracing(c *fiber.Ctx) context.Context { func setupTracing(c fiber.Ctx) context.Context {
ctx := context.Background() ctx := context.Background()
if !cfg.Tracing.Enable || tracer == nil { if !cfg.Tracing.Enable || tracer == nil {
@@ -361,7 +361,7 @@ func setupTracing(c *fiber.Ctx) context.Context {
} }
// performProxyRequest executes the proxy request with retries, circuit breaker, and request coalescing // performProxyRequest executes the proxy request with retries, circuit breaker, and request coalescing
func performProxyRequest(c *fiber.Ctx, proxyURL string) error { func performProxyRequest(c fiber.Ctx, proxyURL string) error {
// Extract user context for cache key (needed for coalescing and circuit breaker fallback) // Extract user context for cache key (needed for coalescing and circuit breaker fallback)
userID, userRole := extractUserInfo(c) userID, userRole := extractUserInfo(c)
@@ -420,7 +420,7 @@ func performProxyRequest(c *fiber.Ctx, proxyURL string) error {
// performProxyRequestCore executes the proxy request with retries and circuit breaker // performProxyRequestCore executes the proxy request with retries and circuit breaker
// This is the core implementation used by both direct calls and coalesced requests // This is the core implementation used by both direct calls and coalesced requests
func performProxyRequestCore(c *fiber.Ctx, proxyURL string, cacheKey string) error { func performProxyRequestCore(c fiber.Ctx, proxyURL string, cacheKey string) error {
// If circuit breaker is not enabled, use the original method // If circuit breaker is not enabled, use the original method
if !cfg.CircuitBreaker.Enable || cb == nil { if !cfg.CircuitBreaker.Enable || cb == nil {
return performProxyRequestWithRetries(c, proxyURL) return performProxyRequestWithRetries(c, proxyURL)
@@ -471,7 +471,7 @@ func performProxyRequestCore(c *fiber.Ctx, proxyURL string, cacheKey string) err
// performProxyRequestWithRetries executes the proxy request with retries // performProxyRequestWithRetries executes the proxy request with retries
// This is the original implementation extracted for reuse // This is the original implementation extracted for reuse
func performProxyRequestWithRetries(c *fiber.Ctx, proxyURL string) error { func performProxyRequestWithRetries(c fiber.Ctx, proxyURL string) error {
// Check backend health first if available // Check backend health first if available
healthMgr := GetBackendHealthManager() healthMgr := GetBackendHealthManager()
if healthMgr != nil && !healthMgr.IsHealthy() { if healthMgr != nil && !healthMgr.IsHealthy() {
@@ -483,7 +483,7 @@ func performProxyRequestWithRetries(c *fiber.Ctx, proxyURL string) error {
} }
// executeProxyAttempt performs a single proxy attempt with error handling // executeProxyAttempt performs a single proxy attempt with error handling
func executeProxyAttempt(c *fiber.Ctx, proxyURL string) error { func executeProxyAttempt(c fiber.Ctx, proxyURL string) error {
// Additional safety check inside retry loop // Additional safety check inside retry loop
if c == nil { if c == nil {
return retry.Unrecoverable(errFiberCtxNilDuringRetry) return retry.Unrecoverable(errFiberCtxNilDuringRetry)
@@ -552,7 +552,7 @@ func executeProxyAttempt(c *fiber.Ctx, proxyURL string) error {
} }
// performProxyRequestWithEnhancedRetries executes the proxy request with intelligent retry strategy // performProxyRequestWithEnhancedRetries executes the proxy request with intelligent retry strategy
func performProxyRequestWithEnhancedRetries(c *fiber.Ctx, proxyURL string, backendUnhealthy bool) error { func performProxyRequestWithEnhancedRetries(c fiber.Ctx, proxyURL string, backendUnhealthy bool) error {
// Safety check for nil context // Safety check for nil context
if c == nil { if c == nil {
return errFiberCtxNil return errFiberCtxNil
@@ -713,7 +713,7 @@ func notifyHealthManager(success bool) {
} }
// handleCircuitOpenGracefulDegradation handles requests when the circuit breaker is open // handleCircuitOpenGracefulDegradation handles requests when the circuit breaker is open
func handleCircuitOpenGracefulDegradation(c *fiber.Ctx, cacheKey string) error { func handleCircuitOpenGracefulDegradation(c fiber.Ctx, cacheKey string) error {
// Try to serve from cache if configured and available // Try to serve from cache if configured and available
if cfg.CircuitBreaker.ReturnCachedOnOpen { if cfg.CircuitBreaker.ReturnCachedOnOpen {
if cachedResponse := libpack_cache.CacheLookup(cacheKey); cachedResponse != nil { if cachedResponse := libpack_cache.CacheLookup(cacheKey); cachedResponse != nil {
@@ -750,7 +750,7 @@ func handleCircuitOpenGracefulDegradation(c *fiber.Ctx, cacheKey string) error {
} }
// doProxyRequestWithTimeout performs a proxy request with proper timeout handling // doProxyRequestWithTimeout performs a proxy request with proper timeout handling
func doProxyRequestWithTimeout(c *fiber.Ctx, proxyURL string, client *fasthttp.Client) error { func doProxyRequestWithTimeout(c fiber.Ctx, proxyURL string, client *fasthttp.Client) error {
// Calculate timeout from client configuration // Calculate timeout from client configuration
clientTimeout := time.Duration(cfg.Client.ClientTimeout) * time.Second clientTimeout := time.Duration(cfg.Client.ClientTimeout) * time.Second
if clientTimeout <= 0 { if clientTimeout <= 0 {
@@ -785,7 +785,7 @@ func doProxyRequestWithTimeout(c *fiber.Ctx, proxyURL string, client *fasthttp.C
} }
// handleGzippedResponse decompresses gzipped responses // handleGzippedResponse decompresses gzipped responses
func handleGzippedResponse(c *fiber.Ctx) error { func handleGzippedResponse(c fiber.Ctx) error {
if !bytes.EqualFold(c.Response().Header.Peek("Content-Encoding"), []byte("gzip")) { if !bytes.EqualFold(c.Response().Header.Peek("Content-Encoding"), []byte("gzip")) {
return nil return nil
} }
@@ -828,7 +828,7 @@ func handleGzippedResponse(c *fiber.Ctx) error {
} }
// logDebugRequest logs the request details when in debug mode with sanitization. // logDebugRequest logs the request details when in debug mode with sanitization.
func logDebugRequest(c *fiber.Ctx) { func logDebugRequest(c fiber.Ctx) {
contentType := string(c.Request().Header.ContentType()) contentType := string(c.Request().Header.ContentType())
sanitizedBody := sanitizeForLogging(c.Body(), contentType) sanitizedBody := sanitizeForLogging(c.Body(), contentType)
sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetReqHeaders())) sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetReqHeaders()))
@@ -845,7 +845,7 @@ func logDebugRequest(c *fiber.Ctx) {
} }
// logDebugResponse logs the response details when in debug mode with sanitization. // logDebugResponse logs the response details when in debug mode with sanitization.
func logDebugResponse(c *fiber.Ctx) { func logDebugResponse(c fiber.Ctx) {
contentType := string(c.Response().Header.ContentType()) contentType := string(c.Response().Header.ContentType())
sanitizedBody := sanitizeForLogging(c.Response().Body(), contentType) sanitizedBody := sanitizeForLogging(c.Response().Body(), contentType)
sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetRespHeaders())) sanitizedHeaders := sanitizeHeaders(convertHeaders(c.GetRespHeaders()))
+20 -21
View File
@@ -6,8 +6,8 @@ import (
"time" "time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
fiber "github.com/gofiber/fiber/v2" fiber "github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v2/middleware/cors" "github.com/gofiber/fiber/v3/middleware/cors"
"github.com/google/uuid" "github.com/google/uuid"
graphql "github.com/lukaszraczylo/go-simple-graphql" graphql "github.com/lukaszraczylo/go-simple-graphql"
@@ -42,19 +42,18 @@ func StartHTTPProxy() error {
}) })
serverConfig := fiber.Config{ serverConfig := fiber.Config{
DisableStartupMessage: true, AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION),
AppName: fmt.Sprintf("GraphQL Monitoring Proxy - %s v%s", libpack_config.PKG_NAME, libpack_config.PKG_VERSION), IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
IdleTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second, ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
ReadTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second, WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second,
WriteTimeout: time.Duration(cfg.Client.ClientTimeout) * time.Second, JSONEncoder: json.Marshal,
JSONEncoder: json.Marshal, JSONDecoder: json.Unmarshal,
JSONDecoder: json.Unmarshal,
} }
server := fiber.New(serverConfig) server := fiber.New(serverConfig)
server.Use(cors.New(cors.Config{ server.Use(cors.New(cors.Config{
AllowOrigins: "*", AllowOrigins: []string{"*"},
})) }))
server.Use(AddRequestUUID) server.Use(AddRequestUUID)
@@ -71,7 +70,7 @@ func StartHTTPProxy() error {
// WebSocket support - must be registered before catch-all routes // WebSocket support - must be registered before catch-all routes
if cfg.WebSocket.Enable { if cfg.WebSocket.Enable {
server.Get("/v1/graphql", func(c *fiber.Ctx) error { server.Get("/v1/graphql", func(c fiber.Ctx) error {
if IsWebSocketRequest(c) { if IsWebSocketRequest(c) {
wsp := GetWebSocketProxy() wsp := GetWebSocketProxy()
if wsp != nil { if wsp != nil {
@@ -90,7 +89,7 @@ func StartHTTPProxy() error {
Pairs: map[string]any{"port": cfg.Server.PortGraphQL}, Pairs: map[string]any{"port": cfg.Server.PortGraphQL},
}) })
if err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL)); err != nil { if err := server.Listen(fmt.Sprintf(":%d", cfg.Server.PortGraphQL), fiber.ListenConfig{DisableStartupMessage: true}); err != nil {
return fmt.Errorf("failed to start HTTP proxy server on port %d: %w", return fmt.Errorf("failed to start HTTP proxy server on port %d: %w",
cfg.Server.PortGraphQL, err) cfg.Server.PortGraphQL, err)
} }
@@ -99,18 +98,18 @@ func StartHTTPProxy() error {
} }
// proxyTheRequestToDefault proxies the request to the default GraphQL endpoint. // proxyTheRequestToDefault proxies the request to the default GraphQL endpoint.
func proxyTheRequestToDefault(c *fiber.Ctx) error { func proxyTheRequestToDefault(c fiber.Ctx) error {
return proxyTheRequest(c, cfg.Server.HostGraphQL) return proxyTheRequest(c, cfg.Server.HostGraphQL)
} }
// AddRequestUUID adds a unique request UUID to the context. // AddRequestUUID adds a unique request UUID to the context.
func AddRequestUUID(c *fiber.Ctx) error { func AddRequestUUID(c fiber.Ctx) error {
c.Locals("request_uuid", uuid.NewString()) c.Locals("request_uuid", uuid.NewString())
return c.Next() return c.Next()
} }
// checkAllowedURLs checks if the requested URL is allowed. // checkAllowedURLs checks if the requested URL is allowed.
func checkAllowedURLs(c *fiber.Ctx) bool { func checkAllowedURLs(c fiber.Ctx) bool {
if len(allowedUrls) == 0 { if len(allowedUrls) == 0 {
return true return true
} }
@@ -120,7 +119,7 @@ func checkAllowedURLs(c *fiber.Ctx) bool {
} }
// healthCheck performs a comprehensive health check on the GraphQL server and its dependencies. // healthCheck performs a comprehensive health check on the GraphQL server and its dependencies.
func healthCheck(c *fiber.Ctx) error { func healthCheck(c fiber.Ctx) error {
// Prepare the response structure // Prepare the response structure
response := HealthCheckResponse{ response := HealthCheckResponse{
Status: "healthy", Status: "healthy",
@@ -254,7 +253,7 @@ func healthCheck(c *fiber.Ctx) error {
} }
// processGraphQLRequest handles the incoming GraphQL requests. // processGraphQLRequest handles the incoming GraphQL requests.
func processGraphQLRequest(c *fiber.Ctx) error { func processGraphQLRequest(c fiber.Ctx) error {
startTime := time.Now() startTime := time.Now()
// Extract user information and check permissions // Extract user information and check permissions
@@ -305,7 +304,7 @@ func processGraphQLRequest(c *fiber.Ctx) error {
} }
// extractUserInfo extracts user ID and role from request headers // extractUserInfo extracts user ID and role from request headers
func extractUserInfo(c *fiber.Ctx) (string, string) { func extractUserInfo(c fiber.Ctx) (string, string) {
extractedUserID := "-" extractedUserID := "-"
extractedRoleName := "-" extractedRoleName := "-"
@@ -326,7 +325,7 @@ func extractUserInfo(c *fiber.Ctx) (string, string) {
} }
// handleCaching manages the caching logic for GraphQL requests // handleCaching manages the caching logic for GraphQL requests
func handleCaching(c *fiber.Ctx, parsedResult *parseGraphQLQueryResult, userID, userRole string) (bool, error) { func handleCaching(c fiber.Ctx, parsedResult *parseGraphQLQueryResult, userID, userRole string) (bool, error) {
// Calculate query hash for cache key - now includes user context for security // Calculate query hash for cache key - now includes user context for security
calculatedQueryHash := libpack_cache.CalculateHash(c, userID, userRole) calculatedQueryHash := libpack_cache.CalculateHash(c, userID, userRole)
@@ -380,7 +379,7 @@ func handleCaching(c *fiber.Ctx, parsedResult *parseGraphQLQueryResult, userID,
} }
// proxyAndCacheTheRequest proxies and caches the request if needed. // proxyAndCacheTheRequest proxies and caches the request if needed.
func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string) error { func proxyAndCacheTheRequest(c fiber.Ctx, queryCacheHash string, cacheTime int, currentEndpoint string) error {
if err := proxyTheRequest(c, currentEndpoint); err != nil { if err := proxyTheRequest(c, currentEndpoint); err != nil {
cfg.Logger.Error(&libpack_logger.LogMessage{ cfg.Logger.Error(&libpack_logger.LogMessage{
Message: "Can't proxy the request", Message: "Can't proxy the request",
@@ -396,7 +395,7 @@ func proxyAndCacheTheRequest(c *fiber.Ctx, queryCacheHash string, cacheTime int,
} }
// logAndMonitorRequest logs and monitors the request processing. // logAndMonitorRequest logs and monitors the request processing.
func logAndMonitorRequest(c *fiber.Ctx, userID, opType, opName string, wasCached bool, duration time.Duration, startTime time.Time) { func logAndMonitorRequest(c fiber.Ctx, userID, opType, opName string, wasCached bool, duration time.Duration, startTime time.Time) {
// Low-cardinality labels only: user_id and op_name dropped to prevent Prometheus explosion. // Low-cardinality labels only: user_id and op_name dropped to prevent Prometheus explosion.
labels := map[string]string{ labels := map[string]string{
"op_type": opType, "op_type": opType,
+23 -23
View File
@@ -10,7 +10,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v3"
libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache" libpack_cache "github.com/lukaszraczylo/graphql-monitoring-proxy/cache"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
@@ -20,11 +20,11 @@ import (
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) { func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Use(AddRequestUUID) app.Use(AddRequestUUID)
var captured string var captured string
app.Get("/", func(c *fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
if v, ok := c.Locals("request_uuid").(string); ok { if v, ok := c.Locals("request_uuid").(string); ok {
captured = v captured = v
} }
@@ -32,7 +32,7 @@ func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
}) })
req := httptest.NewRequest("GET", "/", nil) req := httptest.NewRequest("GET", "/", nil)
resp, err := app.Test(req, -1) resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
if err != nil { if err != nil {
t.Fatalf("app.Test: %v", err) t.Fatalf("app.Test: %v", err)
} }
@@ -51,11 +51,11 @@ func TestAddRequestUUID_SetsLocalsAndCallsNext(t *testing.T) {
} }
func TestAddRequestUUID_UniquePerRequest(t *testing.T) { func TestAddRequestUUID_UniquePerRequest(t *testing.T) {
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Use(AddRequestUUID) app.Use(AddRequestUUID)
seen := make([]string, 0, 5) seen := make([]string, 0, 5)
app.Get("/", func(c *fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
if v, ok := c.Locals("request_uuid").(string); ok { if v, ok := c.Locals("request_uuid").(string); ok {
seen = append(seen, v) seen = append(seen, v)
} }
@@ -64,7 +64,7 @@ func TestAddRequestUUID_UniquePerRequest(t *testing.T) {
for i := range 5 { for i := range 5 {
req := httptest.NewRequest("GET", "/", nil) req := httptest.NewRequest("GET", "/", nil)
resp, err := app.Test(req, -1) resp, err := app.Test(req, fiber.TestConfig{Timeout: 0})
if err != nil { if err != nil {
t.Fatalf("request %d: %v", i, err) t.Fatalf("request %d: %v", i, err)
} }
@@ -89,12 +89,12 @@ func TestHealthCheck_Returns200WithJSON(t *testing.T) {
parseConfig() parseConfig()
_ = StartMonitoringServer() _ = StartMonitoringServer()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Get("/health", healthCheck) app.Get("/health", healthCheck)
// Pass check_graphql=false to avoid real network call // Pass check_graphql=false to avoid real network call
req := httptest.NewRequest("GET", "/health?check_graphql=false&check_redis=false", nil) req := httptest.NewRequest("GET", "/health?check_graphql=false&check_redis=false", nil)
resp, err := app.Test(req, 10000) resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
if err != nil { if err != nil {
t.Fatalf("app.Test: %v", err) t.Fatalf("app.Test: %v", err)
} }
@@ -135,11 +135,11 @@ func TestHealthCheck_UnhealthyWhenGraphQLDown(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}() }()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Get("/health", healthCheck) app.Get("/health", healthCheck)
req := httptest.NewRequest("GET", "/health?check_redis=false", nil) req := httptest.NewRequest("GET", "/health?check_redis=false", nil)
resp, err := app.Test(req, 15000) resp, err := app.Test(req, fiber.TestConfig{Timeout: 15000 * time.Millisecond})
if err != nil { if err != nil {
t.Fatalf("app.Test: %v", err) t.Fatalf("app.Test: %v", err)
} }
@@ -190,14 +190,14 @@ func TestProcessGraphQLRequest_ValidBodyProxiesToBackend(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}() }()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Post("/*", processGraphQLRequest) app.Post("/*", processGraphQLRequest)
body := `{"query":"query { __typename }"}` body := `{"query":"query { __typename }"}`
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body)) req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, 10000) resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
if err != nil { if err != nil {
t.Fatalf("app.Test: %v", err) t.Fatalf("app.Test: %v", err)
} }
@@ -236,7 +236,7 @@ func TestProcessGraphQLRequest_MalformedBodyStillHandled(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}() }()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Post("/*", processGraphQLRequest) app.Post("/*", processGraphQLRequest)
// Not valid JSON — proxy should still forward or return gracefully // Not valid JSON — proxy should still forward or return gracefully
@@ -244,7 +244,7 @@ func TestProcessGraphQLRequest_MalformedBodyStillHandled(t *testing.T) {
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body)) req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, 10000) resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
if err != nil { if err != nil {
t.Fatalf("app.Test: %v", err) t.Fatalf("app.Test: %v", err)
} }
@@ -302,7 +302,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}() }()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Post("/*", processGraphQLRequest) app.Post("/*", processGraphQLRequest)
queryBody := `{"query":"query { users { id } }"}` queryBody := `{"query":"query { users { id } }"}`
@@ -310,7 +310,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
// First request — cache miss, hits backend // First request — cache miss, hits backend
req1 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody)) req1 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
req1.Header.Set("Content-Type", "application/json") req1.Header.Set("Content-Type", "application/json")
resp1, err := app.Test(req1, 10000) resp1, err := app.Test(req1, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
if err != nil { if err != nil {
t.Fatalf("first request: %v", err) t.Fatalf("first request: %v", err)
} }
@@ -323,7 +323,7 @@ func TestHandleCaching_CacheHitReturnsStoredResponse(t *testing.T) {
// Second identical request — should hit cache // Second identical request — should hit cache
req2 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody)) req2 := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
req2.Header.Set("Content-Type", "application/json") req2.Header.Set("Content-Type", "application/json")
resp2, err := app.Test(req2, 10000) resp2, err := app.Test(req2, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
if err != nil { if err != nil {
t.Fatalf("second request: %v", err) t.Fatalf("second request: %v", err)
} }
@@ -380,7 +380,7 @@ func TestHandleCaching_CacheMissProxiesRequest(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}() }()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
app.Post("/*", processGraphQLRequest) app.Post("/*", processGraphQLRequest)
// Unique query so no prior cache entry // Unique query so no prior cache entry
@@ -388,7 +388,7 @@ func TestHandleCaching_CacheMissProxiesRequest(t *testing.T) {
req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody)) req := httptest.NewRequest("POST", "/v1/graphql", strings.NewReader(queryBody))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, 10000) resp, err := app.Test(req, fiber.TestConfig{Timeout: 10000 * time.Millisecond})
if err != nil { if err != nil {
t.Fatalf("app.Test: %v", err) t.Fatalf("app.Test: %v", err)
} }
@@ -430,10 +430,10 @@ func TestHandleCaching_DirectCacheHitBranch(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}() }()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
var wasCachedResult bool var wasCachedResult bool
app.Post("/test", func(c *fiber.Ctx) error { app.Post("/test", func(c fiber.Ctx) error {
parsedResult := &parseGraphQLQueryResult{ parsedResult := &parseGraphQLQueryResult{
cacheTime: 60, cacheTime: 60,
cacheRequest: true, cacheRequest: true,
@@ -507,7 +507,7 @@ func TestHandleCaching_NoCacheEnabled_ProxiesDirect(t *testing.T) {
cfgMutex.Unlock() cfgMutex.Unlock()
}() }()
app := fiber.New(fiber.Config{DisableStartupMessage: true}) app := fiber.New()
reqCtx := &fasthttp.RequestCtx{} reqCtx := &fasthttp.RequestCtx{}
reqCtx.Request.SetRequestURI("/v1/graphql") reqCtx.Request.SetRequestURI("/v1/graphql")
+4 -4
View File
@@ -9,8 +9,8 @@ import (
"time" "time"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"github.com/gofiber/fiber/v2" "github.com/gofiber/contrib/v3/websocket"
"github.com/gofiber/websocket/v2" "github.com/gofiber/fiber/v3"
gorillaws "github.com/gorilla/websocket" gorillaws "github.com/gorilla/websocket"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging" libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring" libpack_monitoring "github.com/lukaszraczylo/graphql-monitoring-proxy/monitoring"
@@ -79,7 +79,7 @@ func NewWebSocketProxy(backendURL string, config WebSocketConfig, logger *libpac
} }
// HandleWebSocket upgrades the connection and proxies WebSocket traffic // HandleWebSocket upgrades the connection and proxies WebSocket traffic
func (wsp *WebSocketProxy) HandleWebSocket(c *fiber.Ctx) error { func (wsp *WebSocketProxy) HandleWebSocket(c fiber.Ctx) error {
if !wsp.enabled { if !wsp.enabled {
return fiber.NewError(fiber.StatusNotImplemented, "WebSocket support is disabled") return fiber.NewError(fiber.StatusNotImplemented, "WebSocket support is disabled")
} }
@@ -477,7 +477,7 @@ func (wsp *WebSocketProxy) GetStats() map[string]any {
} }
// IsWebSocketRequest checks if the request is a WebSocket upgrade request // IsWebSocketRequest checks if the request is a WebSocket upgrade request
func IsWebSocketRequest(c *fiber.Ctx) bool { func IsWebSocketRequest(c fiber.Ctx) bool {
return websocket.IsWebSocketUpgrade(c) || return websocket.IsWebSocketUpgrade(c) ||
c.Get("Upgrade") == "websocket" || c.Get("Upgrade") == "websocket" ||
c.Get("Connection") == "Upgrade" c.Get("Connection") == "Upgrade"