Update all direct and indirect Go dependencies. Notable bumps are
gofiber/fiber v3.4.0 to v3.5.0, gofiber/contrib websocket v1.2.0 to
v1.2.6, fasthttp v1.72.0 to v1.74.0, gookit/goutil v0.7.6 to v0.8.0,
go-redis v9.20.1 to v9.22.0, pgx v5.10.0 to v5.11.0, OpenTelemetry
v1.44.0 to v1.46.0, grpc v1.82.1 to v1.83.2 and testify v1.11.1 to
v1.12.1. The go directive moves to 1.26.0 because websocket v1.2.6
requires it.
fiber v3.5.0 adds an SSRF dial guard to clients registered with
proxy.WithClient. The guard rejects loopback and private upstreams by
default. This proxy gets its upstream from operator configuration, and
sidecar deployments point it at localhost. Set
proxy.SecurityPolicy{AllowPrivateIPs: true} so these upstreams keep
working. This change also unblocks the nightly autoupdate workflow,
which failed on this guard.
Claude-Session: https://claude.ai/code/session_01Fu2wXqGUrfVBi5aLafKk8U
The proxy derived per-user GraphQL cache keys from JWT claims without
verifying the token signature (CWE-345). A remote attacker could forge a
token that carried a victim's claim value and read the victim's cached
response.
This change adds opt-in signature verification behind JWT_VERIFY_SIGNATURE.
The flag defaults to false, so an upgrade without a config change keeps the
previous delegate-to-upstream behavior byte for byte.
When verification is on, the operator must set exactly one key source:
JWT_SECRET, JWT_PUBLIC_KEY, or JWT_JWKS_URL. The verifier pins the accepted
algorithms to the key family through WithValidMethods and a type-switched
keyfunc, and it refuses alg=none. This closes the algorithm-confusion class
of attack. A present but invalid token now returns 401. A missing
Authorization header stays anonymous.
Startup fails loudly when verification is on and the key source is absent or
unreadable, so the proxy can never boot half-configured.
Three bugs fixed:
1. **Test suite timeout** — The main-package test suite timed out at 120s
because each failing request triggered 7 retries with exponential
backoff (500ms base → 10s max ≈ 25.5s per request). Tests that trip
the circuit breaker (3 requests × 25.5s = 76.5s) plus
TestStartHTTPProxy_ErrorHandlerConfigured (another 25.5s) pushed
cumulative time past the limit.
Fix: add `Client.RetryBaseDelayMs` / `Client.RetryMaxDelayMs` config
fields (env: CLIENT_RETRY_BASE_DELAY_MS / CLIENT_RETRY_MAX_DELAY_MS,
defaults 500ms / 10000ms). Tests override these to 10ms so retry
exhaustion completes in milliseconds. Full suite now runs in ~35s.
2. **effectiveCircuitBackoff float overflow** — On amd64, converting
`+Inf` (float64) to `time.Duration` (int64) saturates to MinInt64,
not MaxInt64. The existing "< base" guard then returned `base` instead
of `maxSaneCircuitBackoff`, breaking the sanity ceiling for an uncapped
config with a high trip count. Fix: detect `math.IsInf / math.IsNaN`
before the cast and return the ceiling directly.
3. **TestStartHTTPProxy_ErrorHandlerConfigured** — standalone top-level
test calling parseConfig() directly, bypassing the TestSuite override.
Now sets and restores RetryBaseDelayMs/RetryMaxDelayMs explicitly.
Co-authored-by: lukaszraczylo <[email protected]>
Invalid numeric environment values parsed to zero, an invalid prefixed
value fell through to a stale unprefixed value, and the prefixed boolean
parser rejected on and yes that the unprefixed parser accepted. A
non-positive cache TTL or retry-budget rate could crash or disable a
subsystem.
Parsing now warns and uses the coded default for an invalid value,
prefixed and unprefixed booleans accept the same tokens, and the cache
TTL and retry rate are clamped. A dead per-endpoint circuit config was
removed and a BIND_ADDRESS option was added, defaulting to the current
all-interfaces bind. The pprof server now stops on shutdown.
The manager ran every component concurrently, so on SIGTERM the cache
and connection backends closed while the proxy was still draining
in-flight requests, which then failed with closed clients.
Add an ordered phase to each component. Run phases in ascending order,
components within a phase concurrently, under one timeout budget. Drain
the proxy in an earlier phase than the backend closers.
- tracing: validateEndpoint used fmt.Sscanf("%s:%d") which treats %s as
greedy, so every valid host:port endpoint failed and traced==nil disabled
tracing entirely. Validate with net.SplitHostPort (pure helper, port range
0-65535); test TestValidateEndpoint.
- proxy: createFasthttpClient used clientTimeout for every timeout slot,
ignoring the parsed CLIENT_READ_TIMEOUT/CLIENT_WRITE_TIMEOUT (silent
config loss, documented separately in README). Honour per-side read/write
timeouts, falling back to client timeout when unset.
- cache/memory: evictToFreeMemory capped candidates at maxCacheSize/5 and
stopped at the subset end, so a large write could leave the cache over
its memory limit. Collect all entries and sort.Slice (correct + O(n log n)
vs O(cap^2)), evict until the target is met or the cache is drained.
- cache/redis: the Redis cache client was never closed at shutdown (unlike
the metrics aggregator). Add RedisConfig.Close, CacheWrapper.Close, a
cache.Shutdown and register it with the ShutdownManager.
The generic typed env reader only handled string/int/bool; any float64 key
fell through to default, so CIRCUIT_FAILURE_RATIO, CIRCUIT_BACKOFF_
MULTIPLIER and RETRY_BUDGET_TOKENS_PER_SEC were silently impossible to
override via environment (always the compile-time default).
Add a float64 case mirroring the int one: parse the GMP_ prefixed key,
then the plain key, else the default (envutil has no GetFloat).
Test: extended Test_getDetailsFromEnv with float-valid and float-malformed
cases.
On signal, main waited on wg for StartHTTPProxy's server.Listen, which never
returns because nothing called server.Shutdown(); it timed out after 10s and
process-exited, abruptly killing in-flight GraphQL requests.
Expose the proxy Fiber app and register an http-proxy Shutdown component so
server.Listen returns after Shutdown, letting connections drain. Guard the
shared app handle with a mutex (written by the StartHTTPProxy goroutine, read
by the shutdown component). Verified with build, vet, full test and full
race suite.
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.
The released v0.45.1 binary shipped with the source default
appVersion="dev" because .goreleaser.yaml had ldflags="-s -w" only,
so every startup ping was rejected by the receiver with HTTP 400
(invalid version: regex requires leading digit).
Two-layer fix:
1. .goreleaser.yaml now passes -X main.appVersion={{.Version}} so
goreleaser-built binaries report the actual release version.
2. Switch to telemetry.SendForModule which prefers
debug.ReadBuildInfo Main/Deps when available, falling back to
appVersion. This means `go install github.com/lukaszraczylo/
[email protected]` users also get correct versions
without relying on the ldflag.
Bumps oss-telemetry to v0.2.1 for SendForModule.
Send a single fire-and-forget ping at startup to help track adoption
and version spread. No persistent identifiers are collected.
Adds main.appVersion var (defaulting to "dev"); wire ldflags
(-X main.appVersion=$VERSION) to populate it at release time.
Opt out via any of:
DO_NOT_TRACK=1
OSS_TELEMETRY_DISABLED=1
GRAPHQL_MONITORING_PROXY_DISABLE_TELEMETRY=1
* chore(security,refactor): extract sanitization and improve code quality
- [x] Extract sanitization functions to dedicated sanitization.go module
- [x] Add comprehensive golangci-lint v2 configuration with security rules
- [x] Replace interface{} with any type throughout codebase
- [x] Add admin API authentication security warning
- [x] Extract WebSocket and stats streaming constants
- [x] Add best-effort error handling comments for resource cleanup
- [x] Expand sensitive field patterns for improved PII redaction
- [x] Simplify safety checks and remove redundant nil validations
- [x] Improve test coverage for password field redaction patterns
* refactor: replace interface{} with any type alias
- [x] Replace all `map[string]interface{}` with `map[string]any`
- [x] Replace all `interface{}` with `any` in function signatures and type definitions
- [x] Update sync.Pool New function returns from `interface{}` to `any`
- [x] Add package documentation comments to 8 package files
- [x] Update type assertions and casts to work with `any` type
* General improvements and bug fixes.
* Improve tests coverage.
* fixup! Improve tests coverage.
* Update README.md with latest changes.
* Fix the uint32
* Resolve issue with race condition for logging.
* fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* Fix the test of the rate limiter
* Add default ratelimit.json file
* Update dependencies.
* Significant refactor.
* fixup! Significant refactor.
* fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
* Enhance admin dashboard with real-time WebSocket streaming and charts
Dashboard improvements:
- Added Chart.js for time-series visualization
- Created real-time graphs for RPS and cache hit rate
- Added new statistics displays:
* System uptime with human-readable format
* Total requests with success/failure breakdown
* Current and average RPS
* Success rate with progress bars
* Cache hit rate and memory usage with visual indicators
* Detailed cache statistics
- Implemented WebSocket streaming endpoint (/admin/ws/stats)
- Real-time updates every 2 seconds via WebSocket
- Automatic fallback to polling if WebSocket unavailable
- Connection status indicator
- Progress bars for success rate, cache hit rate, and memory usage
Backend enhancements:
- New WebSocket handler for streaming all statistics
- gatherAllStats() method to collect comprehensive metrics
- Streams data every 2 seconds to connected clients
- Automatic reconnection handling
- Maintains up to 60 data points per chart
The dashboard now provides comprehensive real-time monitoring with:
- Live metrics streaming
- Historical trend visualization
- Responsive design with visual indicators
- Graceful degradation to polling mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Fix WebSocket message handling for real-time stats streaming
Issues fixed:
- Removed blocking default case that prevented ticker from firing
- Separated read and write operations into proper goroutines
- Added proper ping/pong handlers with read deadlines
- Implemented done channel for clean disconnection signaling
- Send initial stats immediately on connection
The WebSocket now properly:
- Streams stats every 2 seconds via ticker
- Handles client disconnections gracefully
- Maintains connection with ping/pong
- Detects connection drops via read goroutine
- Non-blocking message handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Add Redis-based cluster mode for distributed metrics aggregation
When using Redis for caching, proxies now automatically form a cluster
and aggregate metrics across all instances for unified monitoring.
Features:
- Metrics Aggregator: Publishes instance metrics to Redis every 5s
- Cluster Mode API: /admin/api/cluster/stats and /admin/api/cluster/instances
- Dashboard Cluster View: Toggle between single instance and cluster view
- Auto-discovery: Detects cluster mode automatically via Redis
- Instance Management: Each instance gets unique ID (hostname + UUID)
- Graceful Cleanup: Removes metrics from Redis on shutdown
- TTL-based expiration: Stale instances auto-expire after 30s
Cluster metrics include:
- Aggregated requests (total, succeeded, failed, success rate)
- Combined RPS across all instances
- Total cache hits/misses with cluster-wide hit rate
- Per-instance health status and uptime
- Active connections and WebSocket stats
- Request coalescing backend savings
Dashboard improvements:
- Cluster Status section showing total/healthy instances
- Instance Details section with per-node metrics
- Cluster View toggle in header
- Automatic detection of cluster availability
- Individual instance cards with health indicators
- "Current" badge for the instance you're viewing
Architecture:
- Uses Redis SET to track active instances
- Each instance publishes to redis key: graphql-proxy:metrics:instances:{id}
- 30s TTL ensures stale instances are removed
- Aggregator started automatically when Redis cache enabled
- Registered with shutdown manager for graceful cleanup
Environment: Automatically enabled when ENABLE_REDIS_CACHE=true
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Fix cluster instance details using correct JSON field names
JavaScript was using Go struct field names (PascalCase) instead of
JSON field names (snake_case), causing all instance metrics to show
as 0 or undefined.
Fixed references:
- instance.InstanceID → instance.instance_id
- instance.Hostname → instance.hostname
- instance.UptimeSeconds → instance.uptime_seconds
- instance.Stats → instance.stats
- instance.Health → instance.health
Also added fallback to check both instance.cache_summary and
stats.cache_summary for better compatibility.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Enhance Cluster View toggle visibility and styling
Improvements:
- Replaced basic checkbox with custom toggle switch
- Added prominent card-style container with backdrop blur
- Positioned toggle in header next to dashboard title
- Toggle switch with smooth animation (slides left/right)
- Green color when enabled (#10b981)
- Hover effects with slight lift
- Better typography with font weights and spacing
- Info text positioned below toggle label
- Disabled state with reduced opacity
- Responsive layout with flexbox
The toggle is now much more visible and professional-looking,
making it clear when cluster mode is available and active.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Add comprehensive debugging for cluster mode metrics issues
Backend improvements:
- Fixed metrics structure: ensure Stats always has correct inner structure
- Added defensive nil checks for instance.Stats in aggregation
- Added debug logging in publishMetrics to verify data being sent
- Added warning logging in aggregateStats when data is missing
- Log actual keys present in Stats when 'requests' is missing
- Initialize empty maps instead of leaving fields nil
Frontend improvements:
- Added console.log statements to trace cluster data flow
- Log cluster data structure on receive
- Log stats keys and structure
- Log instances array and count
- Warn when expected data is missing
- Added fallback values (|| 0) for display fields
This will help diagnose why cluster view shows zeros by logging:
1. What data is being published to Redis
2. What data is being retrieved from Redis
3. What structure the data has at each step
4. What keys are present vs expected
Check browser console and server logs to see where the data flow breaks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Add cluster debug endpoint for troubleshooting
New endpoint: GET /admin/api/cluster/debug
Returns comprehensive debug information:
- Whether metrics aggregator is initialized
- Redis cache enabled status
- Current instance ID
- Cluster mode status
- Total/healthy instance counts
- Sample instance structure with keys
- Sample requests data structure
- Error messages if any
This helps diagnose cluster mode issues by showing:
1. If Redis is actually enabled
2. If aggregator is initialized
3. What data structure is being stored
4. What keys are present in Stats
5. Sample of actual data being aggregated
Visit http://localhost:8181/admin/api/cluster/debug to see
what's happening under the hood.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Fix cluster mode initialization and improve Redis error visibility
Critical fixes:
1. Move metrics aggregator initialization BEFORE cache initialization
- Runs independently even if CacheEnable=false
- Only requires CacheRedisEnable=true
- This was causing aggregator to not initialize when cache was disabled
2. Promote Redis errors from Warning to Error level
- Changed "Failed to publish" from Warning to Error with ❌ CRITICAL prefix
- Added detailed error context (instance_id, keys, error message)
- Added success logging with ✓ confirmation
- Log command count and data size on success
3. Enhanced startup logging
- Log "Initializing metrics aggregator" with Redis URL/DB
- Log "✓ Successfully initialized" with instance ID
- Log "FAILED to initialize" as ERROR (was Warning)
Why this matters:
- If Redis cache is disabled but Redis is available, cluster mode should still work
- Previous code only initialized aggregator inside cache initialization block
- Redis publish errors were being silently logged as warnings
- No visibility into whether metrics were actually being stored
After this fix:
- Cluster mode works even with ENABLE_GLOBAL_CACHE=false + ENABLE_REDIS_CACHE=true
- Redis errors are immediately visible in logs
- Clear success/failure indicators
- Better troubleshooting information
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Add force-publish endpoint for cluster mode debugging
New endpoint: POST /admin/api/cluster/force-publish
Forces an immediate metrics publish to Redis and reports results.
This helps diagnose why instances aren't appearing:
Response includes:
- success: true/false
- publish_done: confirmation publish was attempted
- instances_found: count after publish
- error: if retrieval failed
- check_logs: reminder to look for log messages
Use this to test:
curl -X POST http://localhost:8181/admin/api/cluster/force-publish
Then check server logs for:
✓ Successfully published metrics to Redis
OR
❌ CRITICAL: Failed to publish metrics to Redis
This bypasses the 5-second timer and publishes immediately,
making it easier to test without waiting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Fix cluster metrics aggregation and dashboard display issues
- Fix metric names: use correct Prometheus metric names (requests_succesful, requests_failed, requests_skipped)
- Add automatic stale instance cleanup (>1 minute inactive)
- Implement 10-second moving average smoothing for RPS, success rate, and cache hit rate
- Add trend indicators (↑ ↗ → ↘ ↓) to show metric direction
- Add compact number formatting (1.2M, 3.4K) with full-value tooltips
- Add retry budget aggregation (allowed/denied retries, denial rate)
- Add circuit breaker aggregation (state counts, per-instance breakdown)
- Add coalescing stats aggregation (backend savings percentage)
- Fix memory display to show "N/A" for Redis cache (memory tracking not available)
- Fix JavaScript error: change hitRate to smoothedHitRate in chart update call
- Change Redis operations to use context.Background() instead of parent context
- Fix staticcheck warning: omit nil check for map len()
This resolves cluster view showing zeros and prevents metrics from disappearing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Fix cluster instance count not updating in upper right corner
The cluster-info element (showing instance count next to "Cluster View" toggle)
was only updated during initial page load. Now it updates in real-time whenever
cluster stats are received via WebSocket.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* Clean up verbose debug logging
Removed debug console.log statements from dashboard JavaScript:
- WebSocket connection/disconnection logs
- Cluster mode availability checks
- Cluster stats update debug logs
Removed verbose Info logs from Go code that ran frequently:
- "Publishing metrics to Redis" (every 5s)
- "Metrics gathered successfully" (every 5s)
- "Successfully published metrics to Redis" (every 5s)
- "Aggregating stats from instances" (frequently)
- "Successfully aggregated cluster metrics" (frequently)
- "Aggregation complete" (frequently)
Kept important logs:
- Error and warning logs
- Initialization and shutdown logs
- Conditional logs (stale instance cleanup, failures)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
---------
Co-authored-by: Claude <[email protected]>
Optimized the getDetailsFromEnv function to reduce redundant lookups and improve type handling
Added direct environment variable access for better performance
Memory Cache Optimization:
Implemented a size-based compression threshold (1KB) to avoid compressing small payloads
Added cache size limits (10,000 entries) to prevent memory leaks
Implemented efficient eviction strategies for the oldest entries
Added atomic counter for thread-safe cache size tracking
Improved cleanup routines with GC triggering for large caches
Proxy Implementation:
Refactored the proxy code into smaller, focused functions for better maintainability
Optimized gzip handling for better performance
Improved error handling and logging
Enhanced tracing integration
GraphQL Processing:
Optimized introspection query checking with fast-path returns
Improved object pool usage
Added detailed comments for better code understanding
Split complex functions into smaller, more focused ones
Fixed test compatibility issues with introspection checking
Request Processing:
Refactored the request processing logic into smaller, focused functions
Separated user extraction, caching, and request handling for better maintainability
Improved error handling and response generation
Tracing Enhancements:
Added better span context management
Implemented custom attributes for more detailed tracing
Added sampling configuration to reduce overhead
Improved resource attribution with host and process information
Added timeout handling for tracing operations
Application Lifecycle:
Implemented graceful shutdown with proper signal handling
Added goroutine management with wait groups
Improved startup sequence with better error handling
Added timeout handling for shutdown operations
Hasura takes its sweet time to start up, that causes the client to error out
as the graphql proxy fires up practically instantly. This is a temporary workaround.
* This change introduces ability to set additional endpoint leading to the
instance of the graphql server connected to the read only database.
If regular query is detected and endpoint for `HOST_GRAPHQL_READONLY` value is set,
the query will be proxied to it. Mutations and non-graphql will be sent
to the `HOST_GRAPHQL` endpoint.