fix(proxy): keep Burst/Endpoints in ratelimit unmarshal; redact large truncated log bodies

- ratelimit: custom UnmarshalJSON temp struct only carried interval/req,
  silently dropping the configured 'burst' and 'endpoints' fields. Burst is
  consumed at checkRateLimit (burst allowance) but could never be set via
  config file. Carry all fields in the temp struct.
- sanitization: a large non-parsable body was truncated before redaction,
  potentially leaking a secret in the first MaxLogBodySize bytes, and the
  truncation could split a multi-byte UTF-8 rune. Redact the retained
  prefix and truncate on a rune boundary (sanitizeTruncatedBody).
- tests: TestRateLimitConfigUnmarshalKeepsBurstAndEndpoints,
  TestSanitizeTruncatedBodyRedactsAndKeepsUTF8, TestTruncateUTF8RuneBoundary.
This commit is contained in:
2026-08-13 05:55:36 +01:00
parent dd2aa922c5
commit fc28c65681
4 changed files with 102 additions and 7 deletions
+24
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"testing"
"time"
"github.com/goccy/go-json"
@@ -275,3 +276,26 @@ func (suite *Tests) Test_RateLimitConfig_UnmarshalJSON() {
suite.Equal(60*time.Second, config.Interval)
})
}
func TestRateLimitConfigUnmarshalKeepsBurstAndEndpoints(t *testing.T) {
// Regression: the custom UnmarshalJSON temp struct used to carry only
// interval/req, silently dropping the configured burst and endpoints
// fields. A config with "burst" or "endpoints" must keep them.
raw := `{"interval":"minute","req":100,"burst":250,"endpoints":["/v1/graphql"]}`
var rc RateLimitConfig
if err := json.Unmarshal([]byte(raw), &rc); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if rc.Interval != time.Minute {
t.Errorf("interval = %v, want %v", rc.Interval, time.Minute)
}
if rc.Req != 100 {
t.Errorf("req = %d, want 100", rc.Req)
}
if rc.Burst != 250 {
t.Errorf("burst = %d, want 250", rc.Burst)
}
if len(rc.Endpoints) != 1 || rc.Endpoints[0] != "/v1/graphql" {
t.Errorf("endpoints = %v, want [\"/v1/graphql\"]", rc.Endpoints)
}
}