diff --git a/router/core/graph_server.go b/router/core/graph_server.go index 0c5b7cd493..fe8bf195e0 100644 --- a/router/core/graph_server.go +++ b/router/core/graph_server.go @@ -1179,6 +1179,7 @@ func (s *graphServer) buildGraphMux(ctx context.Context, Debug: s.rateLimit.Debug, RejectStatusCode: s.rateLimit.SimpleStrategy.RejectStatusCode, KeySuffixExpression: s.rateLimit.KeySuffixExpression, + FailOpen: s.rateLimit.FailOpen, ExprManager: exprManager, }) if err != nil { diff --git a/router/core/ratelimiter.go b/router/core/ratelimiter.go index 0e47f30b60..c997ebab60 100644 --- a/router/core/ratelimiter.go +++ b/router/core/ratelimiter.go @@ -6,11 +6,12 @@ import ( "encoding/json" "errors" "fmt" - rd "github.com/wundergraph/cosmo/router/internal/persistedoperation/operationstorage/redis" "io" "reflect" "sync" + rd "github.com/wundergraph/cosmo/router/internal/persistedoperation/operationstorage/redis" + "github.com/expr-lang/expr/vm" "github.com/go-redis/redis_rate/v10" "github.com/wundergraph/cosmo/router/internal/expr" @@ -28,6 +29,7 @@ type CosmoRateLimiterOptions struct { RejectStatusCode int KeySuffixExpression string + FailOpen bool ExprManager *expr.Manager } @@ -38,6 +40,7 @@ func NewCosmoRateLimiter(opts *CosmoRateLimiterOptions) (rl *CosmoRateLimiter, e limiter: limiter, debug: opts.Debug, rejectStatusCode: opts.RejectStatusCode, + failOpen: opts.FailOpen, } if rl.rejectStatusCode == 0 { rl.rejectStatusCode = 200 @@ -55,10 +58,11 @@ type CosmoRateLimiter struct { client rd.RDCloser limiter *redis_rate.Limiter debug bool - rejectStatusCode int keySuffixProgram *vm.Program + + failOpen bool } func (c *CosmoRateLimiter) RateLimitPreFetch(ctx *resolve.Context, info *resolve.FetchInfo, input json.RawMessage) (result *resolve.RateLimitDeny, err error) { @@ -72,11 +76,15 @@ func (c *CosmoRateLimiter) RateLimitPreFetch(ctx *resolve.Context, info *resolve Period: ctx.RateLimitOptions.Period, } key, err := c.generateKey(ctx) - if err != nil { + if err != nil && c.failOpen{ + return nil, nil + } else if err != nil { return nil, err } allow, err := c.limiter.AllowN(ctx.Context(), key, limit, requestRate) - if err != nil { + if err != nil && c.failOpen{ + return nil, nil + } else if err != nil { return nil, err } c.setRateLimitStats(ctx, key, requestRate, allow.Remaining, allow.RetryAfter.Milliseconds(), allow.ResetAfter.Milliseconds()) diff --git a/router/core/router.go b/router/core/router.go index 473f61a7f1..4760862c3e 100644 --- a/router/core/router.go +++ b/router/core/router.go @@ -798,6 +798,7 @@ func (r *Router) bootstrap(ctx context.Context) error { URLs: r.Config.rateLimit.Storage.URLs, ClusterEnabled: r.Config.rateLimit.Storage.ClusterEnabled, Logger: r.logger, + FailOpen: r.Config.rateLimit.FailOpen, }) if err != nil { return fmt.Errorf("failed to create redis client: %w", err) @@ -1253,6 +1254,7 @@ func (r *Router) Start(ctx context.Context) error { zap.Int("burst", r.rateLimit.SimpleStrategy.Burst), zap.Duration("duration", r.Config.rateLimit.SimpleStrategy.Period), zap.Bool("rejectExceeding", r.Config.rateLimit.SimpleStrategy.RejectExceedingRequests), + zap.Bool("failOpen", r.Config.rateLimit.FailOpen), ) } diff --git a/router/internal/persistedoperation/operationstorage/redis/rdcloser.go b/router/internal/persistedoperation/operationstorage/redis/rdcloser.go index 46fba0f7c0..945317cef7 100644 --- a/router/internal/persistedoperation/operationstorage/redis/rdcloser.go +++ b/router/internal/persistedoperation/operationstorage/redis/rdcloser.go @@ -3,10 +3,11 @@ package rd import ( "context" "fmt" - "github.com/redis/go-redis/v9" - "go.uber.org/zap" "net/url" "strings" + + "github.com/redis/go-redis/v9" + "go.uber.org/zap" ) // RDCloser is an interface that combines the redis.Cmdable and io.Closer interfaces, ensuring that we can close the @@ -20,6 +21,7 @@ type RedisCloserOptions struct { URLs []string ClusterEnabled bool Password string + FailOpen bool } func NewRedisCloser(opts *RedisCloserOptions) (RDCloser, error) { @@ -73,6 +75,10 @@ func NewRedisCloser(opts *RedisCloserOptions) (RDCloser, error) { } if isFunctioning, err := IsFunctioningClient(rdb); !isFunctioning { + if(opts.FailOpen) { + opts.Logger.Warn(fmt.Sprintf("Ratelimit Fail Open activated: redis client is currently not responding with provided URLs: %q", err)) + return rdb, nil + } return rdb, fmt.Errorf("failed to create a functioning redis client with the provided URLs: %w", err) } diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index b83f0360ac..e9c4d66e39 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -473,6 +473,7 @@ type RateLimitConfiguration struct { Debug bool `yaml:"debug" envDefault:"false" env:"RATE_LIMIT_DEBUG"` KeySuffixExpression string `yaml:"key_suffix_expression,omitempty" env:"RATE_LIMIT_KEY_SUFFIX_EXPRESSION"` ErrorExtensionCode RateLimitErrorExtensionCode `yaml:"error_extension_code"` + FailOpen bool `yaml:"fail_open" envDefault:"false" env:"RATE_LIMIT_FAIL_OPEN"` } type RateLimitErrorExtensionCode struct { diff --git a/router/pkg/config/config.schema.json b/router/pkg/config/config.schema.json index 6fbe8851ec..234d125d3d 100644 --- a/router/pkg/config/config.schema.json +++ b/router/pkg/config/config.schema.json @@ -1810,6 +1810,11 @@ "default": "RATE_LIMIT_EXCEEDED" } } + }, + "fail_open": { + "type": "boolean", + "description": "Enable Rate Limit fail open on redis availability failure. This interacts with Redis timeout configuration parameters, essentially adding to each requests latency in failure.", + "default": false } } }, diff --git a/router/pkg/config/testdata/config_defaults.json b/router/pkg/config/testdata/config_defaults.json index 2501daddae..782aae49e1 100644 --- a/router/pkg/config/testdata/config_defaults.json +++ b/router/pkg/config/testdata/config_defaults.json @@ -248,7 +248,8 @@ "ErrorExtensionCode": { "Enabled": true, "Code": "RATE_LIMIT_EXCEEDED" - } + }, + "FailOpen": false }, "LocalhostFallbackInsideDocker": true, "CDN": { diff --git a/router/pkg/config/testdata/config_full.json b/router/pkg/config/testdata/config_full.json index 7869972aef..69cbe74895 100644 --- a/router/pkg/config/testdata/config_full.json +++ b/router/pkg/config/testdata/config_full.json @@ -503,7 +503,8 @@ "ErrorExtensionCode": { "Enabled": true, "Code": "RATE_LIMIT_EXCEEDED" - } + }, + "FailOpen": false }, "LocalhostFallbackInsideDocker": true, "CDN": {