From 2c6f99418f15d643a8c7b5ae0e44bc2d809f3736 Mon Sep 17 00:00:00 2001 From: Gani Georgiev Date: Wed, 25 Jun 2025 20:32:51 +0300 Subject: [PATCH] added the triggered rate limit rule in the error log details --- CHANGELOG.md | 5 ++++ apis/middlewares_rate_limit.go | 3 ++- core/settings_model.go | 10 ++++++++ core/settings_model_test.go | 44 ++++++++++++++++++++++++++++++---- 4 files changed, 56 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90017c0a..d3af840f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v0.29.0 (WIP) + +- Added the triggered rate rimit rule in the error log `details`. + + ## v0.28.4 - Added global JSVM `toBytes()` helper to return the bytes slice representation of a value such as io.Reader or string (_other types are first serialized to Go string_). diff --git a/apis/middlewares_rate_limit.go b/apis/middlewares_rate_limit.go index 886503dc..b4b737a9 100644 --- a/apis/middlewares_rate_limit.go +++ b/apis/middlewares_rate_limit.go @@ -1,6 +1,7 @@ package apis import ( + "errors" "sync" "time" @@ -167,7 +168,7 @@ func checkRateLimit(e *core.RequestEvent, rtId string, rule core.RateLimitRule) } if !rt.isAllowed(key) { - return e.TooManyRequestsError("", nil) + return e.TooManyRequestsError("", errors.New("triggered rate limit rule: "+rule.String())) } return nil diff --git a/core/settings_model.go b/core/settings_model.go index 7464e116..829d5d73 100644 --- a/core/settings_model.go +++ b/core/settings_model.go @@ -704,3 +704,13 @@ func (c RateLimitRule) Validate() error { func (c RateLimitRule) DurationTime() time.Duration { return time.Duration(c.Duration) * time.Second } + +// String returns a string representation of the rule. +func (c RateLimitRule) String() string { + raw, err := json.Marshal(c) + if err != nil { + return c.Label // extremely rare case + } + + return string(raw) +} diff --git a/core/settings_model_test.go b/core/settings_model_test.go index 7706085e..3f489c5b 100644 --- a/core/settings_model_test.go +++ b/core/settings_model_test.go @@ -687,7 +687,7 @@ func TestRateLimitsFindRateLimitRule(t *testing.T) { func TestRateLimitRuleValidate(t *testing.T) { scenarios := []struct { name string - config core.RateLimitRule + rule core.RateLimitRule expectedErrors []string }{ { @@ -784,7 +784,7 @@ func TestRateLimitRuleValidate(t *testing.T) { for _, s := range scenarios { t.Run(s.name, func(t *testing.T) { - result := s.config.Validate() + result := s.rule.Validate() tests.TestValidationErrors(t, result, s.expectedErrors) }) @@ -793,7 +793,7 @@ func TestRateLimitRuleValidate(t *testing.T) { func TestRateLimitRuleDurationTime(t *testing.T) { scenarios := []struct { - config core.RateLimitRule + rule core.RateLimitRule expected time.Duration }{ {core.RateLimitRule{}, 0 * time.Second}, @@ -801,8 +801,8 @@ func TestRateLimitRuleDurationTime(t *testing.T) { } for i, s := range scenarios { - t.Run(fmt.Sprintf("%d_%d", i, s.config.Duration), func(t *testing.T) { - result := s.config.DurationTime() + t.Run(fmt.Sprintf("%d_%d", i, s.rule.Duration), func(t *testing.T) { + result := s.rule.DurationTime() if result != s.expected { t.Fatalf("Expected duration %d, got %d", s.expected, result) @@ -810,3 +810,37 @@ func TestRateLimitRuleDurationTime(t *testing.T) { }) } } + +func TestRateLimitRuleString(t *testing.T) { + scenarios := []struct { + name string + rule core.RateLimitRule + expected string + }{ + { + "empty", + core.RateLimitRule{}, + `{"label":"","audience":"","duration":0,"maxRequests":0}`, + }, + { + "all fields", + core.RateLimitRule{ + Label: "POST /a/b/", + Duration: 1, + MaxRequests: 2, + Audience: core.RateLimitRuleAudienceAuth, + }, + `{"label":"POST /a/b/","audience":"@auth","duration":1,"maxRequests":2}`, + }, + } + + for _, s := range scenarios { + t.Run(s.name, func(t *testing.T) { + result := s.rule.String() + + if result != s.expected { + t.Fatalf("Expected string\n%s\ngot\n%s", s.expected, result) + } + }) + } +}