Compare commits

...
14 Commits
10 changed files with 400 additions and 119 deletions
+7
View File
@@ -167,6 +167,7 @@ You can still use the non-prefixed environment variables in the spirit of the ba
| `HOST_GRAPHQL` | The host to proxy the graphql endpoint | `http://localhost/` |
| `HOST_GRAPHQL_READONLY` | The host to proxy the read-only graphql endpoint | `` |
| `HEALTHCHECK_GRAPHQL_URL` | The URL to check the health of the graphql endpoint | `` |
| `BACKEND_HEALTHCHECK_URL` | Optional HTTP readiness URL for startup and periodic backend checks; empty keeps the GraphQL probe | `` |
| `JWT_USER_CLAIM_PATH` | Path to the user claim in the JWT token | `` |
| `JWT_ROLE_CLAIM_PATH` | Path to the role claim in the JWT token | `` |
| `JWT_VERIFY_SIGNATURE` | Verify the JWT signature before trusting its claims (see [Caching](#caching)) | `false` |
@@ -1127,6 +1128,12 @@ With the `PURGE_METRICS_ON_CRAWL` enabled, the `graphql_proxy_requests_failed`,
If you'd like the `/healthz` endpoint to perform actual check for the connectivity to the graphql endpoint - set the `HEALTHCHECK_GRAPHQL_URL` environment variable to the exact URL of the graphql endpoint. The query executed will be `query { __typename }` and if the response is not `200 OK` - the healthcheck will fail. Remember that the endpoint is a full URL which you'd like to check, so it should include the protocol, host and path - for example `http://localhost:8080/v1/graphql` and it's NOT the same as value of `HOST_GRAPHQL` environment variable which should provide only the host, without path, ending with slash.
`BACKEND_HEALTHCHECK_URL` controls a separate check: the backend health manager's startup and periodic probes. Set it to an unauthenticated readiness endpoint, for example `http://127.0.0.1:8080/ready`. The manager sends `GET` to the supplied path and query, without a GraphQL body or authentication headers. Any 2xx response is healthy; network errors, timeouts, and other status codes are unhealthy. The request timeout remains five seconds, and redirects are not followed.
When this variable is empty, the manager continues to send an unauthenticated `POST` containing `{"query":"{__typename}"}` to `HOST_GRAPHQL`. It preserves a supplied path, or appends `/v1/graphql` when there is no path beyond `/`. A configured readiness URL must use HTTP or HTTPS with a nonempty host and no userinfo or fragment. Invalid configuration stops startup; it does not fall back to GraphQL. Do not put secrets in the URL: probe targets are logged.
This setting does not change `HEALTHCHECK_GRAPHQL_URL`, the API `/healthz` check, GraphQL authorization, or headers on forwarded client requests. Keep the GraphQL endpoint protected; only the readiness endpoint must accept these unauthenticated probes.
#### Monitoring endpoint
Example metrics produced by the proxy:
+2 -2
View File
@@ -62,7 +62,7 @@ func TestApiBackendHealth_HealthyManager_Returns200(t *testing.T) {
defer func() { backendHealthManager = orig }()
// inject a healthy manager directly (bypassing sync.Once)
mgr := NewBackendHealthManager(&fasthttp.Client{}, "http://localhost:8080", libpack_logger.New())
mgr := NewBackendHealthManager(&fasthttp.Client{}, "http://localhost:8080", "", libpack_logger.New())
mgr.isHealthy.Store(true)
backendHealthManager = mgr
@@ -86,7 +86,7 @@ func TestApiBackendHealth_UnhealthyManager_Returns503(t *testing.T) {
orig := backendHealthManager
defer func() { backendHealthManager = orig }()
mgr := NewBackendHealthManager(&fasthttp.Client{}, "http://localhost:8080", libpack_logger.New())
mgr := NewBackendHealthManager(&fasthttp.Client{}, "http://localhost:8080", "", libpack_logger.New())
mgr.isHealthy.Store(false)
backendHealthManager = mgr
+64 -49
View File
@@ -22,6 +22,8 @@ type BackendHealthManager struct {
logger *libpack_logger.Logger
cancel context.CancelFunc
backendURL string
healthCheckURL string
useHTTPReadiness bool
checkInterval time.Duration
maxRetries int
mu sync.RWMutex
@@ -32,18 +34,41 @@ type BackendHealthManager struct {
}
// NewBackendHealthManager creates a new backend health manager
func NewBackendHealthManager(client *fasthttp.Client, backendURL string, logger *libpack_logger.Logger) *BackendHealthManager {
func NewBackendHealthManager(client *fasthttp.Client, backendURL, readinessURL string, logger *libpack_logger.Logger) *BackendHealthManager {
ctx, cancel := context.WithCancel(context.Background())
healthCheckURL := readinessURL
if healthCheckURL == "" {
healthCheckURL = backendGraphQLHealthURL(backendURL)
} else {
// Path normalization is a client setting in fasthttp. Use a separate
// pool so readiness URLs stay exact without changing forwarded traffic.
client = &fasthttp.Client{
Dial: client.Dial,
DialTimeout: client.DialTimeout,
TLSConfig: client.TLSConfig,
ReadTimeout: client.ReadTimeout,
WriteTimeout: client.WriteTimeout,
MaxIdleConnDuration: client.MaxIdleConnDuration,
MaxConnDuration: client.MaxConnDuration,
ReadBufferSize: client.ReadBufferSize,
WriteBufferSize: client.WriteBufferSize,
MaxResponseBodySize: client.MaxResponseBodySize,
NoDefaultUserAgentHeader: client.NoDefaultUserAgentHeader,
DisablePathNormalizing: true,
}
}
return &BackendHealthManager{
client: client,
backendURL: backendURL,
checkInterval: 5 * time.Second,
maxRetries: 30, // 30 * 5s = 2.5 minutes max startup wait
ctx: ctx,
cancel: cancel,
logger: logger,
startupProbe: true,
readinessChan: make(chan bool, 1),
client: client,
backendURL: backendURL,
healthCheckURL: healthCheckURL,
useHTTPReadiness: readinessURL != "",
checkInterval: 5 * time.Second,
maxRetries: 30, // 30 * 5s = 2.5 minutes max startup wait
ctx: ctx,
cancel: cancel,
logger: logger,
startupProbe: true,
readinessChan: make(chan bool, 1),
}
}
@@ -59,6 +84,7 @@ func (bhm *BackendHealthManager) WaitForBackendReady(timeout time.Duration) erro
Message: "Waiting for GraphQL backend to become ready",
Pairs: map[string]any{
"backend_url": bhm.backendURL,
"check_url": bhm.healthCheckURL,
"timeout": timeout.String(),
},
})
@@ -149,43 +175,15 @@ func (bhm *BackendHealthManager) checkBackendHealth() bool {
defer fasthttp.ReleaseRequest(req)
defer fasthttp.ReleaseResponse(resp)
// Determine the health check URL
// If backendURL is just "http://host:port" or "http://host:port/", append /v1/graphql
// If it has a path like "/v1/graphql", use that path
healthCheckURL := bhm.backendURL
hasGraphQLPath := false
if len(bhm.backendURL) > 0 {
// Simple check: if URL has a path component beyond just "/"
lastSlash := -1
protoEnd := 0
if idx := strings.Index(bhm.backendURL, "://"); idx >= 0 {
protoEnd = idx + 3
}
for i := protoEnd; i < len(bhm.backendURL); i++ {
if bhm.backendURL[i] == '/' {
lastSlash = i
break
}
}
// Has path if there's a slash after protocol and it's not the last char or followed by more path
hasGraphQLPath = lastSlash >= protoEnd && lastSlash < len(bhm.backendURL)-1
// If no GraphQL path, append /v1/graphql (standard Hasura endpoint)
if !hasGraphQLPath {
// Remove trailing slash if present
baseURL := strings.TrimSuffix(bhm.backendURL, "/")
healthCheckURL = baseURL + "/v1/graphql"
}
req.SetRequestURI(bhm.healthCheckURL)
if bhm.useHTTPReadiness {
req.Header.SetMethod(http.MethodGet)
} else {
req.Header.SetMethod(http.MethodPost)
req.Header.SetContentType("application/json")
req.SetBodyString(`{"query":"{__typename}"}`)
}
// Always send GraphQL introspection query for health check
healthQuery := `{"query":"{__typename}"}`
req.SetRequestURI(healthCheckURL)
req.Header.SetMethod(http.MethodPost)
req.Header.SetContentType("application/json")
req.SetBody([]byte(healthQuery))
// Short timeout for health checks
err := bhm.client.DoTimeout(req, resp, 5*time.Second)
if err != nil {
@@ -193,7 +191,7 @@ func (bhm *BackendHealthManager) checkBackendHealth() bool {
Message: "Backend health check failed",
Pairs: map[string]any{
"error": err.Error(),
"check_url": healthCheckURL,
"check_url": bhm.healthCheckURL,
},
})
return false
@@ -207,7 +205,7 @@ func (bhm *BackendHealthManager) checkBackendHealth() bool {
Message: "Backend returned unhealthy status",
Pairs: map[string]any{
"status_code": statusCode,
"check_url": healthCheckURL,
"check_url": bhm.healthCheckURL,
},
})
}
@@ -284,6 +282,9 @@ func (bhm *BackendHealthManager) Shutdown() {
return
}
bhm.cancel()
if bhm.useHTTPReadiness {
bhm.client.CloseIdleConnections()
}
if bhm.logger != nil {
bhm.logger.Info(&libpack_logger.LogMessage{
Message: "Backend health manager shut down",
@@ -298,9 +299,9 @@ var (
)
// InitializeBackendHealth initializes the backend health manager
func InitializeBackendHealth(client *fasthttp.Client, backendURL string, logger *libpack_logger.Logger) *BackendHealthManager {
func InitializeBackendHealth(client *fasthttp.Client, backendURL, readinessURL string, logger *libpack_logger.Logger) *BackendHealthManager {
backendHealthOnce.Do(func() {
backendHealthManager = NewBackendHealthManager(client, backendURL, logger)
backendHealthManager = NewBackendHealthManager(client, backendURL, readinessURL, logger)
})
return backendHealthManager
}
@@ -309,3 +310,17 @@ func InitializeBackendHealth(client *fasthttp.Client, backendURL string, logger
func GetBackendHealthManager() *BackendHealthManager {
return backendHealthManager
}
func backendGraphQLHealthURL(backendURL string) string {
if backendURL == "" {
return ""
}
protoEnd := 0
if idx := strings.Index(backendURL, "://"); idx >= 0 {
protoEnd = idx + 3
}
if pathStart := strings.IndexByte(backendURL[protoEnd:], '/'); pathStart >= 0 && protoEnd+pathStart < len(backendURL)-1 {
return backendURL
}
return strings.TrimSuffix(backendURL, "/") + "/v1/graphql"
}
+234
View File
@@ -0,0 +1,234 @@
package main
import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"os/exec"
"strings"
"sync/atomic"
"testing"
"time"
libpack_logger "github.com/lukaszraczylo/graphql-monitoring-proxy/logging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
)
func TestBackendReadinessProtectedGraphQL(t *testing.T) {
var readyStatus atomic.Int32
readyStatus.Store(http.StatusOK)
var graphqlCalls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v1/graphql":
graphqlCalls.Add(1)
w.WriteHeader(http.StatusUnauthorized)
case "/ready":
assert.Equal(t, http.MethodGet, r.Method)
assert.Empty(t, r.Header.Get("Authorization"))
assert.Empty(t, r.Header.Get("X-Husaria-Admin-Secret"))
assert.Empty(t, r.Header.Get("X-Hasura-Admin-Secret"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.Empty(t, body)
w.WriteHeader(int(readyStatus.Load()))
default:
w.WriteHeader(http.StatusNotFound)
}
}))
t.Cleanup(server.Close)
client := &fasthttp.Client{}
t.Cleanup(client.CloseIdleConnections)
logger := libpack_logger.New().SetOutput(io.Discard)
legacy := NewBackendHealthManager(client, server.URL, "", logger)
t.Cleanup(legacy.Shutdown)
require.False(t, legacy.checkBackendHealth(), "GraphQL must reject the unauthenticated probe")
manager := NewBackendHealthManager(client, server.URL, server.URL+"/ready", logger)
t.Cleanup(manager.Shutdown)
manager.checkInterval = 10 * time.Millisecond
manager.StartHealthChecking()
require.NoError(t, manager.WaitForBackendReady(time.Second))
require.True(t, manager.IsHealthy())
readyStatus.Store(http.StatusServiceUnavailable)
require.Eventually(t, func() bool { return !manager.IsHealthy() }, time.Second, 5*time.Millisecond)
readyStatus.Store(http.StatusOK)
require.Eventually(t, manager.IsHealthy, time.Second, 5*time.Millisecond)
assert.Equal(t, int32(1), graphqlCalls.Load(), "readiness checks must not query protected GraphQL")
require.False(t, legacy.checkBackendHealth(), "readiness must not bypass GraphQL authorization")
}
func TestBackendReadinessExactTarget(t *testing.T) {
const target = "/checks//ready%2Fdatabase?probe=one%2Ftwo&probe=three"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodGet, r.Method)
assert.Equal(t, target, r.RequestURI)
w.WriteHeader(http.StatusNoContent)
}))
t.Cleanup(server.Close)
client := &fasthttp.Client{}
t.Cleanup(client.CloseIdleConnections)
manager := NewBackendHealthManager(client, "http://unused.invalid", server.URL+target, libpack_logger.New().SetOutput(io.Discard))
t.Cleanup(manager.Shutdown)
assert.True(t, manager.checkBackendHealth())
}
func TestBackendReadinessStatus(t *testing.T) {
for _, tc := range []struct {
name string
status int
healthy bool
}{
{name: "good OK", status: http.StatusOK, healthy: true},
{name: "good no content", status: http.StatusNoContent, healthy: true},
{name: "edge final success", status: 299, healthy: true},
{name: "bad redirect", status: http.StatusFound},
{name: "bad unauthorized", status: http.StatusUnauthorized},
{name: "bad unavailable", status: http.StatusServiceUnavailable},
} {
t.Run(tc.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/ready" {
t.Error("readiness probe followed a redirect")
w.WriteHeader(http.StatusOK)
return
}
w.Header().Set("Location", "/redirected")
w.WriteHeader(tc.status)
}))
t.Cleanup(server.Close)
client := &fasthttp.Client{}
t.Cleanup(client.CloseIdleConnections)
manager := NewBackendHealthManager(client, server.URL, server.URL+"/ready", libpack_logger.New().SetOutput(io.Discard))
t.Cleanup(manager.Shutdown)
assert.Equal(t, tc.healthy, manager.checkBackendHealth())
})
}
}
func TestBackendReadinessNetworkFailure(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
server.Close()
client := &fasthttp.Client{}
t.Cleanup(client.CloseIdleConnections)
manager := NewBackendHealthManager(client, server.URL, server.URL+"/ready", libpack_logger.New().SetOutput(io.Discard))
t.Cleanup(manager.Shutdown)
assert.False(t, manager.checkBackendHealth())
}
func TestBackendReadinessTimeout(t *testing.T) {
release := make(chan struct{})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
<-release
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(server.Close)
t.Cleanup(func() { close(release) })
client := &fasthttp.Client{ReadTimeout: 20 * time.Millisecond}
t.Cleanup(client.CloseIdleConnections)
manager := NewBackendHealthManager(client, server.URL, server.URL+"/ready", libpack_logger.New().SetOutput(io.Discard))
t.Cleanup(manager.Shutdown)
assert.False(t, manager.checkBackendHealth())
}
func TestBackendReadinessLegacyGraphQL(t *testing.T) {
for _, tc := range []struct {
name string
configuredPath string
requestURI string
}{
{name: "good bare host", requestURI: "/v1/graphql"},
{name: "edge root path", configuredPath: "/", requestURI: "/v1/graphql"},
{name: "good custom path and query", configuredPath: "/graphql?health=1", requestURI: "/graphql?health=1"},
} {
t.Run(tc.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, http.MethodPost, r.Method)
assert.Equal(t, tc.requestURI, r.RequestURI)
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
assert.Empty(t, r.Header.Get("Authorization"))
body, err := io.ReadAll(r.Body)
assert.NoError(t, err)
assert.JSONEq(t, `{"query":"{__typename}"}`, string(body))
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(server.Close)
client := &fasthttp.Client{}
t.Cleanup(client.CloseIdleConnections)
manager := NewBackendHealthManager(client, server.URL+tc.configuredPath, "", libpack_logger.New().SetOutput(io.Discard))
t.Cleanup(manager.Shutdown)
assert.True(t, manager.checkBackendHealth())
})
}
}
func readinessURLWithUserinfo(user *url.Userinfo) string {
u := url.URL{Scheme: "http", Host: "example.test", Path: "/ready", User: user}
return u.String()
}
func TestBackendReadinessURLValidation(t *testing.T) {
for _, tc := range []struct {
name string
url string
valid bool
}{
{name: "edge unset", valid: true},
{name: "good HTTP", url: "http://127.0.0.1:8080/ready", valid: true},
{name: "good HTTPS query", url: "https://example.test/check?database=primary", valid: true},
{name: "good IPv6", url: "http://[::1]:8080/ready", valid: true},
{name: "bad relative", url: "/ready"},
{name: "bad scheme", url: "ftp://example.test/ready"},
{name: "bad missing host", url: "http:///ready"},
{name: "bad port only host", url: "http://:8080/ready"},
{name: "bad opaque", url: "http:ready"},
{name: "bad userinfo", url: readinessURLWithUserinfo(url.UserPassword("operator", "secret"))},
{name: "bad user only", url: readinessURLWithUserinfo(url.User("operator"))},
{name: "bad escaped userinfo", url: strings.Replace(readinessURLWithUserinfo(url.UserPassword("operator", "%zz")), "%25zz", "%zz", 1)},
{name: "bad path escape", url: "http://example.test/%zz"},
{name: "bad port", url: "http://example.test:invalid/ready"},
{name: "bad port range", url: "http://example.test:65536/ready"},
{name: "bad fragment", url: "http://example.test/ready#ignored"},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateBackendHealthcheckURL(tc.url)
if tc.valid {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.NotContains(t, err.Error(), tc.url)
assert.NotContains(t, err.Error(), "operator")
assert.NotContains(t, err.Error(), "secret")
}
})
}
}
func TestBackendReadinessInvalidConfigStopsStartup(t *testing.T) {
const helper = "GMP_TEST_INVALID_READINESS_CONFIG"
if os.Getenv(helper) == "1" {
parseConfig()
t.Fatal("invalid readiness configuration did not stop startup")
}
command := exec.Command(os.Args[0], "-test.run=^TestBackendReadinessInvalidConfigStopsStartup$")
for _, variable := range os.Environ() {
if !strings.HasPrefix(variable, "GMP_BACKEND_HEALTHCHECK_URL=") && !strings.HasPrefix(variable, helper+"=") {
command.Env = append(command.Env, variable)
}
}
command.Env = append(command.Env, helper+"=1", "GMP_BACKEND_HEALTHCHECK_URL="+readinessURLWithUserinfo(url.UserPassword("operator", "secret")))
output, err := command.CombinedOutput()
var exitError *exec.ExitError
require.ErrorAs(t, err, &exitError)
assert.Equal(t, 1, exitError.ExitCode())
assert.Contains(t, string(output), "Invalid BACKEND_HEALTHCHECK_URL")
assert.NotContains(t, string(output), "operator")
assert.NotContains(t, string(output), "secret")
}
+7 -15
View File
@@ -77,16 +77,8 @@ func (suite *ConnectionResilienceTestSuite) TearDownTest() {
// TestBackendHealthManager tests the backend health monitoring
func (suite *ConnectionResilienceTestSuite) TestBackendHealthManager() {
suite.Run("initialization", func() {
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, cfg.Logger)
suite.NotNil(healthMgr)
suite.Equal(cfg.Server.HostGraphQL, healthMgr.backendURL)
suite.Equal(5*time.Second, healthMgr.checkInterval)
suite.Equal(30, healthMgr.maxRetries)
})
suite.Run("health check success", func() {
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, cfg.Logger)
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, "", cfg.Logger)
isHealthy := healthMgr.checkBackendHealth()
suite.True(isHealthy)
suite.GreaterOrEqual(suite.mockServerCalled.Load(), int32(1))
@@ -94,13 +86,13 @@ func (suite *ConnectionResilienceTestSuite) TestBackendHealthManager() {
suite.Run("health check failure", func() {
// Use invalid URL to simulate failure
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, "http://invalid-url:99999", cfg.Logger)
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, "http://invalid-url:99999", "", cfg.Logger)
isHealthy := healthMgr.checkBackendHealth()
suite.False(isHealthy)
})
suite.Run("startup readiness with healthy backend", func() {
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, cfg.Logger)
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, "", cfg.Logger)
err := healthMgr.WaitForBackendReady(10 * time.Second)
suite.NoError(err)
suite.True(healthMgr.IsHealthy())
@@ -108,7 +100,7 @@ func (suite *ConnectionResilienceTestSuite) TestBackendHealthManager() {
suite.Run("startup readiness timeout", func() {
// Use invalid URL to simulate backend not ready
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, "http://invalid-url:99999", cfg.Logger)
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, "http://invalid-url:99999", "", cfg.Logger)
err := healthMgr.WaitForBackendReady(2 * time.Second)
suite.Error(err)
suite.Contains(err.Error(), "did not become ready")
@@ -177,7 +169,7 @@ func (suite *ConnectionResilienceTestSuite) TestConnectionPoolManager() {
func (suite *ConnectionResilienceTestSuite) TestIntegratedHealthManagement() {
suite.Run("global initialization", func() {
// Initialize global instances
healthMgr := InitializeBackendHealth(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, cfg.Logger)
healthMgr := InitializeBackendHealth(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, "", cfg.Logger)
poolMgr := NewConnectionPoolManager(cfg.Client.FastProxyClient)
// Set global instances
@@ -194,7 +186,7 @@ func (suite *ConnectionResilienceTestSuite) TestIntegratedHealthManagement() {
// and may have already fired earlier in the process (e.g. via parseConfig in
// another test), in which case it returns whatever the global currently is —
// which TearDownTest above just nilled.
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, cfg.Logger)
healthMgr := NewBackendHealthManager(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, "", cfg.Logger)
backendHealthManager = healthMgr
// Start health checking
@@ -272,7 +264,7 @@ func TestConnectionResilienceSuite(t *testing.T) {
// periodic checking — otherwise the backend could never be marked healthy
// again even after it recovers.
func (suite *ConnectionResilienceTestSuite) TestHealthChecksStartAfterStartupTimeout() {
bhm := NewBackendHealthManager(cfg.Client.FastProxyClient, "http://invalid-url:99999", cfg.Logger)
bhm := NewBackendHealthManager(cfg.Client.FastProxyClient, "http://invalid-url:99999", "", cfg.Logger)
err := bhm.WaitForBackendReady(200 * time.Millisecond)
suite.Error(err, "expected timeout for unreachable backend")
+3 -3
View File
@@ -187,7 +187,7 @@ func TestCoverageMicro_BackendHealth(t *testing.T) {
client := &fasthttp.Client{}
t.Run("updateHealthStatus healthy→unhealthy transition", func(t *testing.T) {
bhm := NewBackendHealthManager(client, "http://localhost:9999", logger)
bhm := NewBackendHealthManager(client, "http://localhost:9999", "", logger)
defer bhm.Shutdown()
// Start healthy
@@ -203,7 +203,7 @@ func TestCoverageMicro_BackendHealth(t *testing.T) {
})
t.Run("updateHealthStatus unhealthy→healthy resets counter", func(t *testing.T) {
bhm := NewBackendHealthManager(client, "http://localhost:9999", logger)
bhm := NewBackendHealthManager(client, "http://localhost:9999", "", logger)
defer bhm.Shutdown()
bhm.isHealthy.Store(false)
@@ -219,7 +219,7 @@ func TestCoverageMicro_BackendHealth(t *testing.T) {
})
t.Run("GetLastHealthCheck round-trip", func(t *testing.T) {
bhm := NewBackendHealthManager(client, "http://localhost:9999", logger)
bhm := NewBackendHealthManager(client, "http://localhost:9999", "", logger)
defer bhm.Shutdown()
before := time.Now()
+13 -13
View File
@@ -6,10 +6,10 @@ toolchain go1.26.6
require (
github.com/MicahParks/keyfunc/v3 v3.8.2
github.com/VictoriaMetrics/metrics v1.44.0
github.com/VictoriaMetrics/metrics v1.44.1
github.com/alicebob/miniredis/v2 v2.33.0
github.com/avast/retry-go/v4 v4.7.0
github.com/goccy/go-json v0.10.6
github.com/goccy/go-json v0.11.2
github.com/gofiber/contrib/v3/websocket v1.2.6
github.com/gofiber/fiber/v3 v3.5.0
github.com/gofrs/flock v0.13.1
@@ -32,7 +32,7 @@ require (
go.opentelemetry.io/otel/sdk v1.46.0
go.opentelemetry.io/otel/trace v1.46.0
go.uber.org/automaxprocs v1.6.0
google.golang.org/grpc v1.83.2
google.golang.org/grpc v1.84.0
)
require (
@@ -43,19 +43,19 @@ require (
github.com/fasthttp/websocket v1.5.12 // indirect
github.com/go-logr/logr v1.4.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gofiber/schema v1.8.6 // indirect
github.com/gofiber/utils/v2 v2.5.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 // indirect
github.com/gofiber/schema v1.8.8 // indirect
github.com/gofiber/utils/v2 v2.6.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.20.0 // indirect
github.com/klauspost/compress v1.20.1 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/molecule-man/go-brrr v1.1.0 // indirect
github.com/molecule-man/go-brrr v1.1.1 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/savsgio/gotils v0.0.0-20250924091648-bce9a52d7761 // indirect
github.com/tinylib/msgp v1.6.4 // indirect
github.com/tinylib/msgp v1.6.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastrand v1.1.0 // indirect
github.com/valyala/histogram v1.2.0 // indirect
@@ -63,8 +63,8 @@ require (
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect
go.opentelemetry.io/otel/metric v1.46.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.opentelemetry.io/proto/otlp v1.11.1 // indirect
go.uber.org/atomic v1.12.0 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.57.0 // indirect
golang.org/x/net v0.59.0 // indirect
@@ -73,7 +73,7 @@ require (
golang.org/x/term v0.46.0 // indirect
golang.org/x/text v0.42.0 // indirect
golang.org/x/time v0.16.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260908043556-f8649ddbbfe6 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260908043556-f8649ddbbfe6 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc // indirect
google.golang.org/protobuf v1.36.12 // indirect
)
+32 -32
View File
@@ -2,8 +2,8 @@ github.com/MicahParks/jwkset v0.11.3 h1:Phli4RdTDdIdLXZpuO7abkwZyzIk0RDTUPVVBHPR
github.com/MicahParks/jwkset v0.11.3/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=
github.com/MicahParks/keyfunc/v3 v3.8.2 h1:eydEwk/pBAVrDIpmFfB/gkCcrp++xQ7YYXirrI2zlWE=
github.com/MicahParks/keyfunc/v3 v3.8.2/go.mod h1:T4snFPe26GwMg45bBAdM5P6qWQyLxZHLwBhxR/9PnCs=
github.com/VictoriaMetrics/metrics v1.44.0 h1:Fr8yqQSV+ZfYaDD/anqk1E8e9YPgfleSleJmAI0M0Tw=
github.com/VictoriaMetrics/metrics v1.44.0/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc=
github.com/VictoriaMetrics/metrics v1.44.1 h1:OMyGrMkhI3hnq+IpJ1gZK9VhXcsOjUX1CoOFv//bJ0I=
github.com/VictoriaMetrics/metrics v1.44.1/go.mod h1:xDM82ULLYCYdFRgQ2JBxi8Uf1+8En1So9YUwlGTOqTc=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
@@ -21,25 +21,25 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/fxamacker/cbor/v2 v2.9.3 h1:oQBnFATpNdY8gJHTndDDv5Xl4QqNaz51G5LLEPhng3Q=
github.com/fxamacker/cbor/v2 v2.9.3/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/fxamacker/cbor/v2 v2.9.4 h1:xwjVlxEMR3S605oUlgBjKLTTeGFciYPGYCtF/35LKGo=
github.com/fxamacker/cbor/v2 v2.9.4/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.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
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.11.2 h1:jdZv93Tt4ioR8yW1CoNsvSxrcZlCXAUU1aZXN7gpXUA=
github.com/goccy/go-json v0.11.2/go.mod h1:3NdmfEkZlB7YI5UFw/qdFKq8XN1aiWR0YyRPWZNQltY=
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/gofiber/contrib/v3/websocket v1.2.6 h1:5XcHejNCq460uLSdDAAp11RIrDGMHax07d0zWUZXgYc=
github.com/gofiber/contrib/v3/websocket v1.2.6/go.mod h1:QHnpSvNDsoVHrfSvnMl9ptx6x4l83moCtiT+4XM++nw=
github.com/gofiber/fiber/v3 v3.5.0 h1:dk7TOUH6DXJGtOLsN2XEG+0ZML7cznzHILTVozbNEK8=
github.com/gofiber/fiber/v3 v3.5.0/go.mod h1:GOVDTW+gjJvfe0iJyVujbQ1Lnx+JUjFySJRI/9/xX/w=
github.com/gofiber/schema v1.8.6 h1:xVlbSg3vsyKVlfzRBps2tJ7pHx1d8mSgiaJye6Lb3xs=
github.com/gofiber/schema v1.8.6/go.mod h1:Cxh3loYVG3FLA7FzW4C5G83pL/pmCmAnuaIJ5y2HwkU=
github.com/gofiber/utils/v2 v2.5.1 h1:Yrt/ljbfsgnDSHymoMvVX5PFrb9kCV/OJnDu6sYuT4A=
github.com/gofiber/utils/v2 v2.5.1/go.mod h1:5GyHTNQA1I6Qwlw7BWxQsZl8iuFS4yPjLFck+wYR304=
github.com/gofiber/schema v1.8.8 h1:md5t65+Z9LttOuj7FdMcQ63D2+6GniT2ganLXwvTJM4=
github.com/gofiber/schema v1.8.8/go.mod h1:Hzf0L/fTPP1IwOAwKipKflm9C2722aebnSKJCMOgb1I=
github.com/gofiber/utils/v2 v2.6.1 h1:x3zsS/oUQz0ia2Zx8XXtG/7naR5a+/95FRO8mwQ5QQ4=
github.com/gofiber/utils/v2 v2.6.1/go.mod h1:tc9KqdRAQZRD/u0mN2d1ZHROGheSZv+0FcC9cg5OEdw=
github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo=
github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
@@ -56,8 +56,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 h1:/Tnpcb2E0Pz/tN9s3bfEY2Q8ePCEX9iuS+cneUwncnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0/go.mod h1:zOBXOsUaBSjKgmH4OGzV1esUpR3oUSCPYVd2cUBjKYY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0 h1:Bd7KaOxzULLxtZ/K5s1aLbWhR0+5RToO65TXHsf3bqQ=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.31.0/go.mod h1:nN7ts3dFXKtCZWc//yfkpcQNKJABg16/uDVAZpLDalo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -66,8 +66,8 @@ github.com/jackc/pgx/v5 v5.11.0 h1:IzBBtyK9AHqf98cctWFifYSci2hgQR/cd56wB4p+ogg=
github.com/jackc/pgx/v5 v5.11.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/compress v1.20.1 h1:T7kKElXUMXrUJ2E9QhQhxFtcK5rPyLdsGZvdbLMPdiQ=
github.com/klauspost/compress v1.20.1/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9 h1:pL8B9mjv6RPUfKYYGm/uJ7QL6Ndf+z+OEl0qJE6KmEc=
@@ -82,8 +82,8 @@ 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-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/molecule-man/go-brrr v1.1.0 h1:rLBGSXA393y+5ttCBLkW34CHESop5tahrOFxVv/XDEU=
github.com/molecule-man/go-brrr v1.1.0/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
github.com/molecule-man/go-brrr v1.1.1 h1:KYwusQhjtV3smALnhmafZf4OBrh7vP4p8O47DyUmmuY=
github.com/molecule-man/go-brrr v1.1.1/go.mod h1:7ybW6/7gA3oKY45jOfVNjSJDtrr6ea4tzbsTkjmQDC4=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -93,8 +93,8 @@ github.com/redis/go-redis/v9 v9.22.0 h1:laDvpYXTJtZLloinw1fA5Kqd6HAEH2XKxOkG/PDq
github.com/redis/go-redis/v9 v9.22.0/go.mod h1:y2g0Wj8rQvuK0ELM+oxSudcLtC09JScs98I/X9gRWY4=
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/shamaton/msgpack/v3 v3.2.2 h1:lZITRsXLQkdJ9J2qvh6gjGpWxci/dwuiUf+jm9AudJM=
github.com/shamaton/msgpack/v3 v3.2.2/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo=
github.com/shamaton/msgpack/v3 v3.2.3 h1:0r0aZtrXDFOPpwWJTEOpFq+Ih/JmMLm0pqLN5mSilrw=
github.com/shamaton/msgpack/v3 v3.2.3/go.mod h1:sgBYvEiyz8JR1NC3yGRoPVME9xXovpnh3l/plW1nfRo=
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
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=
@@ -102,8 +102,8 @@ 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.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tinylib/msgp v1.6.4 h1:mOwYbyYDLPj35mkA2BjjYejgJk9BuHxDdvRnb6v2ZcQ=
github.com/tinylib/msgp v1.6.4/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/tinylib/msgp v1.6.5 h1:iAH6XTP7BpzErjBZlujJQXxA+GmRi2YWs6M4KdLtNbE=
github.com/tinylib/msgp v1.6.5/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
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/fasthttp v1.74.0 h1:wMS9fnO2QTALozYx5pId2Vi7ZwU/epUkY8i/KPWCHoU=
@@ -134,10 +134,10 @@ go.opentelemetry.io/otel/sdk/metric v1.46.0 h1:0piZ26EG4RBfebb2jhDH6ERCYHoVWduc3
go.opentelemetry.io/otel/sdk/metric v1.46.0/go.mod h1:I1PbKrdVc8Qu8HYVDNtqVIwLwjNrhsV/uFuxfwg8mO4=
go.opentelemetry.io/otel/trace v1.46.0 h1:OULy7ccdJnZtJ0UDYFOIGaCmiWzJ8Vi2G/Rsu60qs1c=
go.opentelemetry.io/otel/trace v1.46.0/go.mod h1:J7GAXweO77XSFkB/rmAqk9D6ihszhFjLU+d9WuUxDLI=
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.opentelemetry.io/proto/otlp v1.11.1 h1:SCrPqH9NE5CmBrOqpjNCiT0X5mGwMyGERXdogfR7Yjw=
go.opentelemetry.io/proto/otlp v1.11.1/go.mod h1:wq8W4aSf6bOy8RZyESOeQFr9Pu7wydWX/TY4sgvrTPg=
go.uber.org/atomic v1.12.0 h1:BvcXdFKuviU4fTL/f+SxdQ5qJX/Jix8pAkgdUcb3XOE=
go.uber.org/atomic v1.12.0/go.mod h1:I6c4cg+6HCxRjfjSsYtApoFILnpc0CGUdGkXVqbYVNk=
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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -158,16 +158,16 @@ golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI=
golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E=
golang.org/x/time v0.16.0 h1:vMb6ptszcQMkcwiRTAuNNU50gom6++Q/6gY2hDM6VDE=
golang.org/x/time v0.16.0/go.mod h1:rVKOqvZeKvrDKTQiAHJ7wmwP0RzleSphoEA9RcdLA0s=
golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI=
golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo=
golang.org/x/tools v0.50.0 h1:c2ifzfcuY7L90lZ2aKd8S4K2NpASF08SZx9ZuJkHmSU=
golang.org/x/tools v0.50.0/go.mod h1:7ulVMw3831Mwi5EZD6RomGyffr4VFjuNYXf2BbCEAV0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260908043556-f8649ddbbfe6 h1:O4Tjo2vlGeGM3+tNgwJrGj5fQHO2V71gDkFqbm328C8=
google.golang.org/genproto/googleapis/api v0.0.0-20260908043556-f8649ddbbfe6/go.mod h1:r4KD2hOq82JBWpTWkJ9NZLf6EwmRvAPIFmk7hPNtd+0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260908043556-f8649ddbbfe6 h1:ieEbjQ6lzbvntOXUB9nMx9uH+yIU/HbgkNDjnk/mJuk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260908043556-f8649ddbbfe6/go.mod h1:DjtHYE8FKJLivXcBEjGwndXfIC23G0VpXiXKqG179uA=
google.golang.org/grpc v1.83.2 h1:EManeRomTObA0BU7I8vXgg/78uE5MJ9M8B39EX2WscU=
google.golang.org/grpc v1.83.2/go.mod h1:YPI1hK3kDked6iHvgX3tR0y+nX/qpMFKhPgFsokw1S8=
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc h1:rZHRz0ogq4Pid1IKtA/ivHi8XONVF5tqUQXA2TtgpkE=
google.golang.org/genproto/googleapis/api v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:K/n0XLazCJHjD73YuMyNJsN38BPPy1kfFnKxUXaThFU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc h1:4bNTbnb44EqGVy9HaQxSY2jnafSUdgV3qHtedvNpDKg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260928230214-8a89bd6388cc/go.mod h1:OaIUM3+LpYcK2GXM4FTmhWoIq371Owdr+Cc7/BsYHHc=
google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0=
google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+33 -1
View File
@@ -246,6 +246,33 @@ func validateJWTClaimPath(path string) error {
return nil
}
func validateBackendHealthcheckURL(rawURL string) error {
if rawURL == "" {
return nil
}
u, err := url.Parse(rawURL)
if err != nil {
// Parse errors can include credentials from the rejected URL.
return fmt.Errorf("must be a valid HTTP or HTTPS URL")
}
if (u.Scheme != "http" && u.Scheme != "https") || u.Hostname() == "" || u.Opaque != "" {
return fmt.Errorf("must use HTTP or HTTPS with a nonempty host")
}
if u.User != nil {
return fmt.Errorf("must not contain userinfo or credentials")
}
if u.Fragment != "" {
return fmt.Errorf("must not contain a fragment")
}
if port := u.Port(); port != "" {
n, err := strconv.Atoi(port)
if err != nil || n < 1 || n > 65535 {
return fmt.Errorf("must use a port between 1 and 65535")
}
}
return nil
}
// parseConfig loads and parses the configuration.
func parseConfig() {
libpack_config.PKG_NAME = "graphql_proxy"
@@ -259,6 +286,11 @@ func parseConfig() {
c.Server.BindAddress = getDetailsFromEnv("BIND_ADDRESS", "")
c.Server.HostGraphQL = getDetailsFromEnv("HOST_GRAPHQL", "http://localhost/")
c.Server.HostGraphQLReadOnly = getDetailsFromEnv("HOST_GRAPHQL_READONLY", "")
c.Server.BackendHealthcheckURL = getDetailsFromEnv("BACKEND_HEALTHCHECK_URL", "")
if err := validateBackendHealthcheckURL(c.Server.BackendHealthcheckURL); err != nil {
fmt.Fprintf(os.Stderr, "CRITICAL ERROR: Invalid BACKEND_HEALTHCHECK_URL: %v\n", err)
os.Exit(1)
}
// Client configurations
c.Client.JWTUserClaimPath = getDetailsFromEnv("JWT_USER_CLAIM_PATH", "")
c.Client.JWTRoleClaimPath = getDetailsFromEnv("JWT_ROLE_CLAIM_PATH", "")
@@ -678,7 +710,7 @@ func parseConfig() {
// Initialize backend health manager
if cfg.Server.HostGraphQL != "" {
healthMgr := InitializeBackendHealth(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, cfg.Logger)
healthMgr := InitializeBackendHealth(cfg.Client.FastProxyClient, cfg.Server.HostGraphQL, cfg.Server.BackendHealthcheckURL, cfg.Logger)
// Start health checking in background
healthMgr.StartHealthChecking()
}
+5 -4
View File
@@ -64,10 +64,11 @@ type config struct {
DisableTLSVerify bool
}
Server struct {
HostGraphQL string
HostGraphQLReadOnly string
HealthcheckGraphQL string
AllowURLs []string // List of allowed URL paths for access control
HostGraphQL string
HostGraphQLReadOnly string
HealthcheckGraphQL string
BackendHealthcheckURL string
AllowURLs []string // List of allowed URL paths for access control
// BindAddress is the host portion of every listener's bind address
// (GraphQL proxy, admin API, monitoring). Empty (the default)
// preserves the existing ":port" behavior of binding all