525 Commits
Author SHA1 Message Date
github-actions[bot]andGitHub e99647e346 chore(deps): update dependencies
Automated dependency update. All tests passed.
v3.1.54
2026-10-08 03:08:47 +00:00
github-actions[bot]andGitHub 73962d0c9a chore(deps): update module github.com/molecule-man/go-brrr to v1.2.0
Automated dependency update. All tests passed.
v3.1.52
2026-10-06 03:05:56 +00:00
github-actions[bot]andGitHub b0c113f443 chore(deps): update dependencies
Automated dependency update. All tests passed.
v3.1.50
2026-10-04 10:56:46 +00:00
lukaszraczylo abb79535d1 fix(deps): update go-ratecounter to v0.1.13
Version 0.1.12 had a data race between addValue and cleanUpOldValues. The
race detector in CI reported it in the burst test. Version 0.1.13 copies
the pooled values and the race is gone.
v3.1.48
2026-10-04 11:52:14 +01:00
lukaszraczylo 4d87dee2c6 fix(deps): update sony/gobreaker to v2
The v2 module path is github.com/sony/gobreaker/v2 and the breaker is
generic. The code instantiates it with any, which matches the v1
Execute signature.
v3.1.46
2026-10-04 10:16:43 +01:00
lukaszraczylo 02780d942c fix(deps): update avast/retry-go to v5
The v5 API replaces retry.Do(fn, opts...) with retry.New(opts...).Do(fn).
The retry call in proxy.go uses the new form with the same options.
2026-10-04 10:15:21 +01:00
lukaszraczylo 67cc71bbf8 test(redis): paginate SCAN in the Clear failure test
miniredis 2.39 ignores the SCAN COUNT hint and returns every key in one
batch. The test now paginates SCAN in its own hook, so it no longer
depends on miniredis batching.
2026-10-04 10:14:04 +01:00
lukaszraczylo 9b70f5693a ci(deps): update dependencies with Renovate [skip ci] 2026-10-03 18:44:34 +01:00
lukaszraczyloandGitHub f93a0e2865 Update go.mod and go.sum (#103) v3.1.39 2026-10-03 04:14:32 +01:00
lukaszraczyloandGitHub 90a6b99bb3 Update go.mod and go.sum (#102) v3.1.37 2026-10-02 04:04:16 +01:00
lukaszraczyloandGitHub 503d09f479 Update go.mod and go.sum (#101) v3.1.35 2026-10-01 04:07:38 +01:00
lukaszraczyloandGitHub dc034fb0c7 Update go.mod and go.sum (#100) v3.1.33 2026-09-30 04:04:34 +01:00
lukaszraczyloandGitHub 6554da0d04 Update go.mod and go.sum (#99) v3.1.31 2026-09-29 04:03:21 +01:00
lukaszraczyloandGitHub 819a8e2315 Update go.mod and go.sum (#98) v3.1.29 2026-09-28 04:05:44 +01:00
lukaszraczyloandGitHub c41f7907dd fix: add HTTP backend readiness probes (#97) v3.1.27 2026-09-26 23:43:10 +01:00
lukaszraczyloandGitHub 85531966fe Update go.mod and go.sum (#96) v3.1.25 2026-09-26 04:03:07 +01:00
lukaszraczyloandGitHub af2f5d2f96 Update go.mod and go.sum (#95) v3.1.23 2026-09-25 04:03:16 +01:00
lukaszraczyloandGitHub b8f01567b7 Update go.mod and go.sum (#94) v3.1.21 2026-09-24 04:02:30 +01:00
lukaszraczyloandGitHub 8cf525c924 Update go.mod and go.sum (#93) v3.1.19 2026-09-22 04:02:34 +01:00
lukaszraczyloandGitHub cc35d3fc38 Update go.mod and go.sum (#92) v3.1.17 2026-09-19 04:03:22 +01:00
lukaszraczyloandGitHub 5882042a9d Update go.mod and go.sum (#91) v3.1.15 2026-09-18 04:02:26 +01:00
lukaszraczyloandGitHub e0ad34c513 Update go.mod and go.sum (#90) v3.1.13 2026-09-16 04:03:13 +01:00
lukaszraczyloandGitHub aa5167c42b Update go.mod and go.sum (#89) v3.1.11 2026-09-15 04:02:28 +01:00
lukaszraczyloandGitHub e2d151e9b7 Update go.mod and go.sum (#88) v3.1.9 2026-09-13 04:02:14 +01:00
lukaszraczylo 02d9796adb fix(deps): update dependencies and allow loopback upstreams on fiber v3.5.0
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
v3.1.7
2026-09-10 15:18:31 +01:00
lukaszraczylo 6e571f8fa0 fix(deps): bump gofiber/fiber to v3.4.0 for HSTS advisory fix (GHSA-gv83-gqw6-9j2c)
Fiber v3.3.0 and earlier never set the HSTS header in the helmet middleware (GHSA-gv83-gqw6-9j2c, GO-2026-5912). Version v3.4.0 fixes that and adds an open-redirect fix and X-Forwarded-For hardening. Version v3.4.0 does NOT contain the loopback SSRF block that v3.5.0 added to the proxy middleware, so the 127.0.0.1 backend behaviour is unchanged. The full race test suite, including the loopback proxy tests, passes. Transitive bumps: fasthttp v1.71.0 to v1.72.0, klauspost/compress to v1.19.0.
v3.1.5
2026-08-17 19:47:30 +01:00
lukaszraczylo 4a68dcb00a fix(build): pin Go toolchain to 1.26.6 for standard-library CVE fixes
Builds used the go1.26.5 toolchain, whose standard library contained seven vulnerabilities that govulncheck reported as reachable (GO-2026-6218, GO-2026-6091, GO-2026-6090, GO-2026-6089, GO-2026-6088, GO-2026-5972, GO-2026-5026). This change pins the module toolchain to go1.26.6 and sets the CI Go version to 1.26.6, so every build compiles against the patched standard library. After the bump govulncheck reports zero vulnerabilities affecting the code. The go language directive stays at 1.25.0, so no language behaviour changes.
2026-08-17 19:45:26 +01:00
lukaszraczylo 3bce659203 fix(deps): bump klauspost/compress to v1.18.7 for s2 out-of-bounds read (GHSA-259r-337f-4rfw)
The indirect dependency github.com/klauspost/compress v1.18.6 contained an out-of-bounds read in the s2 package (GO-2026-5841). The proxy pulls this module in through valyala/fasthttp. govulncheck reports the vulnerable path as unreachable, so the real risk is low, but v1.18.7 removes the defect. The bump is a patch release with no API change.
v3.1.1
2026-08-17 19:30:07 +01:00
lukaszraczylo 8258b3d99a fix(auth): verify JWT signature to prevent cache-key forgery (GHSA-9gqw-h2rw-44wv)
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.
2026-08-17 13:51:00 +01:00
99f0c844f0 fix(tests): configurable retry delays and circuit backoff overflow
Three bugs fixed:

1. Test suite timeout: add Client.RetryBaseDelayMs/RetryMaxDelayMs config
   fields (env CLIENT_RETRY_BASE_DELAY_MS/CLIENT_RETRY_MAX_DELAY_MS,
   defaults 500ms/10s). Tests override to 10ms so retry exhaustion
   completes in ms instead of ~25s per request. Suite runs in ~35s.

2. effectiveCircuitBackoff float overflow: on amd64, float64(+Inf) cast
   to int64 gives MinInt64 not MaxInt64, so the < base guard fired and
   returned base instead of the 24h sanity ceiling. Fix: check
   math.IsInf/math.IsNaN before the cast.

3. TestStartHTTPProxy_ErrorHandlerConfigured bypassed the TestSuite
   retry-delay override; now sets/restores the fields explicitly.

Co-authored-by: lukaszraczylo <[email protected]>
2026-08-14 16:00:50 +00:00
8d1518b961 fix(tests): configurable retry delays and circuit backoff overflow
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]>
2026-08-14 15:58:10 +00:00
lukaszraczylo aaa08dfbb2 docs: ground README and landing page in the actual behavior
An audit against the code found stale defaults, wrong numbers, an
invented error-response example, and capability claims for features that
do not exist (query-complexity limiting, IP-based and per-endpoint rate
limits, config hot-reload).

Correct the env-var defaults, the degraded-retry and keepalive numbers,
and the circuit-open response example. Reframe the cache-key isolation as
a correctness boundary, not an authentication one, because the proxy does
not verify JWT signatures. Document the admin dashboard exposure, the API
key scope, the WebSocket ban and rate-limit checks, and the new
BIND_ADDRESS option.
2026-08-14 13:01:34 +01:00
lukaszraczylo 6171436a99 fix(api): report the real keepalive interval in pool health
The /api/connection-pool/health endpoint hardcoded a keepalive interval
of 15s while the pool actually uses 45s, so it reported a false value.
The endpoint now reads the live keepAliveInterval field. A startup
backoff comment that claimed jitter is also corrected; the backoff has
no jitter.
2026-08-14 13:01:20 +01:00
lukaszraczylo 70b86c779b fix(server): gate WebSocket on ban and rate limit, map errors to status
The WebSocket route skipped the ban and rate-limit checks that the HTTP
path enforced, and returned errors set no custom status, so circuit-open
became HTTP 500. The route now runs the same ban and rate-limit checks
before the upgrade, and a custom error handler maps errors to the
correct status, including 503 for circuit-open. The API and admin
listeners honour the BIND_ADDRESS option.
2026-08-14 11:10:54 +01:00
lukaszraczylo 044fa41744 fix(config): validate env values and clamp unsafe settings
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.
2026-08-14 11:10:39 +01:00
lukaszraczylo 10e51daab3 feat(circuit): progressive backoff and configurable trip conditions
The circuit breaker ignored the trip-on-timeout, trip-on-4xx, and
trip-on-5xx flags and never cleared its counts, so the failure ratio
became lifetime-cumulative. The backoff-multiplier and max-backoff
settings did nothing. Circuit-open surfaced as HTTP 500.

Failure classification now honours the trip flags, the breaker uses a
rolling count window, and repeated trips extend the open state up to the
configured maximum. A helper maps circuit-open to HTTP 503. The request
body is copied once per request instead of once per retry. Defaults
reproduce the prior behaviour: the breaker is off by default and a
backoff multiplier of one keeps a constant open window.
2026-08-14 11:10:37 +01:00
lukaszraczylo 6751346796 fix(ratelimit): make burst a short-window allowance above the rate
Burst and the sustained rate were compared against the same window, so
a burst above the sustained rate never took effect. Burst is now
measured over a short window on top of the sustained limit. An unset
burst preserves the previous rate-only behaviour.
2026-08-14 11:10:24 +01:00
lukaszraczylo e6a43356c0 fix(metrics): order aggregator shutdown and reset the singleton
Shutdown closed the Redis client while the publisher goroutine could
still use it, so the instance key sometimes lingered in the cluster set,
and the goroutine could read a package global after shutdown returned.
Shutdown now removes the instance key, joins the publisher goroutine,
then closes the client, and clears the singleton so a later
initialisation creates a fresh instance.
2026-08-14 11:10:22 +01:00
lukaszraczylo 2911dd9b1d perf(monitoring): cache hostname and bound the label-key cache
get_metrics_name called os.Hostname on every metrics call and keyed a
never-evicted cache by label values. The hostname is now resolved once
and the cache is keyed by label names, so it cannot grow with high
cardinality values. The metrics listener also honours a graceful
shutdown context.
2026-08-14 11:10:15 +01:00
lukaszraczylo a774f9905b fix(logging): make level and field-name access concurrency-safe
SetFieldName wrote a package-global map with no lock while log read it
from many goroutines, and the level, caller, and time-format setters
mutated fields without synchronisation. A runtime setter call could
crash the process with a concurrent map write.

The scalar fields now use atomics on the hot log path and the global
field-name map uses a read-write mutex. Behaviour for init-time
configuration is unchanged.
2026-08-14 11:10:01 +01:00
lukaszraczylo 159faa68e0 perf(cache): sort-based eviction and consistent non-positive TTL handling
The count-based evictOldest used an O(n squared) selection sort with a
candidate cap over an unordered map range, so it could evict the wrong
entries. It now collects all entries and sorts by expiry, matching the
memory-based evictor.

A non-positive CACHE_TTL behaved differently per backend: the standard
memory cache panicked, Redis stored a permanent key, and the LRU cache
expired entries immediately. Each backend now clamps a non-positive TTL
to a shared default at its Set boundary. The nil-logger fallback in the
Redis wrapper now uses the constructor so it cannot panic.
2026-08-14 11:09:57 +01:00
lukaszraczylo 1cab28b054 fix(shutdown): sequence components into phases so backends close after drain
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.
2026-08-14 01:42:06 +01:00
lukaszraczylo 77447bd353 fix(retry): start refill loop when budget enabled via UpdateConfig
The refill goroutine started only when the budget was enabled at
construction. Enabling it later through UpdateConfig left a bucket that
never refilled.

Start the refill loop from UpdateConfig, guarded by sync.Once so it
starts at most once, sharing the existing shutdown lifecycle.
2026-08-14 01:42:04 +01:00
lukaszraczylo 6226717aad fix(logging): bound JSON log path and mask secrets straddling the cut
The JSON log path re-marshaled redacted output with no size cap, so a
large body was logged in full. An XML-form secret whose closing tag
fell past the truncation cut also leaked its prefix.

Cap the re-marshaled JSON at MaxLogBodySize after redaction. Strip a
trailing unterminated sensitive tag. Fold tag names with a
length-preserving ASCII lower so the cut index stays valid.
2026-08-14 01:41:56 +01:00
lukaszraczylo 95f72b0b1a fix(cache): make memory Clear counter accounting race-safe
Clear reset the counters with StoreInt64(0) after deleting entries, so
a Set racing the reset left a live entry while counters read zero, and
the next Delete drove them permanently negative.

Rewrite Clear to LoadAndDelete each key and subtract the linearized
returned entry, matching the rest of the file. Size the key slice from
the live entry count.
2026-08-14 01:41:43 +01:00
lukaszraczylo 636ddb9d95 fix(cache): escape glob metacharacters and aggregate errors in redis Clear
The SCAN MATCH pattern used the raw prefix, so a prefix containing
Redis glob metacharacters cleared the wrong keys or none. A Del error
also aborted the clear and discarded the remaining batches.

Escape the prefix for MATCH, continue past a per-batch Del error, and
return the joined error. Apply the same escaping to CountQueries.
2026-08-14 01:41:31 +01:00
lukaszraczylo f792de815a fix(logging): bound truncateUTF8 backoff and mask secrets that straddle the cut
truncateUTF8 validated the whole string each iteration. One
invalid byte emptied the log line at quadratic cost. A secret
whose closing quote fell past the truncation cut also leaked its
leading characters, because the redaction pattern needs a
terminator.

Back off over the last rune only, bounded by the maximum rune
width, and trim the partial rune before redaction runs. Strip a
trailing unterminated quoted value after the cut.
2026-08-14 00:01:44 +01:00
lukaszraczylo fcdf76b8e8 fix(coalesce): surface panic error to coalesced waiters
When the primary request panicked, waiters received a nil response
and nil error, which the proxy turned into an empty HTTP 200.

Recover in the deferred release, store an error response for the
waiters, then re-panic so the primary panic semantics are
unchanged. The test gates the panic on the live waiter count, so
every waiter is coalesced before the release.
2026-08-14 00:01:31 +01:00
lukaszraczylo 2c2fe2623f fix(cache): make memory counter accounting lock-free and bound eviction alloc
The Set mutex serialised writes but left Delete, expiry, and
eviction racing the same counters, so double subtraction remained.
Eviction also pre-allocated from the configured maximum, which
could allocate hundreds of megabytes on a near-empty cache.

Use sync.Map.Swap in Set and adjust counters from the linearized
previous value. Size the eviction slice from the live entry count.
CleanExpiredEntries now subtracts the size of the entry it
actually deleted. A concurrency test recomputes expected counters
from the final map contents.
2026-08-14 00:01:17 +01:00
lukaszraczylo ccc25426dd fix(websocket): join ping goroutine and unify keepalive deadlines
The handler returned while the ping goroutine could still write to
a connection that gofiber had recycled into its pool. This caused
a nil dereference panic or a ping on an unrelated socket.

Join the ping goroutine through a done channel before the handler
returns. Compute effective keepalive values once in the
constructor, clamp pongTimeout to at least pingInterval with one
warning, and pass the effective values to both proxy directions.
A negative interval or timeout disables keepalive. New test drives
the real HandleWebSocket wiring.
2026-08-14 00:01:03 +01:00