-
Notifications
You must be signed in to change notification settings - Fork 57
feat(gateway): make request body size configurable via env #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3ee492e
d8b96eb
17e1fdb
f290dbe
60a1453
2538326
37dffe6
a0484b1
4506231
66adb49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,19 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "errors" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
| "net/http/httptest" | ||
| "os" | ||
| "strings" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| ) | ||
|
|
||
| func TestValidateConfig_MissingRequiredEnv(t *testing.T) { | ||
|
|
@@ -474,3 +483,138 @@ func TestGetReceiptTTL(t *testing.T) { | |
| func stringPtr(value string) *string { | ||
| return &value | ||
| } | ||
|
|
||
| func TestGetMaxBodySize(t *testing.T) { | ||
| t.Run("default when unset", func(t *testing.T) { | ||
| t.Setenv("MAX_REQUEST_BODY_MB", "") | ||
| if got := getMaxBodySize(); got != 10*1024*1024 { | ||
| t.Fatalf("expected 10MB default, got %d", got) | ||
| } | ||
| }) | ||
|
|
||
|
Comment on lines
+487
to
+494
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add route-level regression coverage for configured 413 responsesThese added tests only validate That leaves both the cached and uncached 413 responses unprotected, so a future drift in the actual rejection path or max_size payload would still pass CI even though this PR changes a public error contract there |
||
| t.Run("custom value", func(t *testing.T) { | ||
| t.Setenv("MAX_REQUEST_BODY_MB", "25") | ||
| if got := getMaxBodySize(); got != 25*1024*1024 { | ||
| t.Fatalf("expected 25MB, got %d", got) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("zero falls back to default", func(t *testing.T) { | ||
| t.Setenv("MAX_REQUEST_BODY_MB", "0") | ||
| if got := getMaxBodySize(); got != 10*1024*1024 { | ||
| t.Fatalf("expected 10MB fallback for zero, got %d", got) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("negative falls back to default", func(t *testing.T) { | ||
| t.Setenv("MAX_REQUEST_BODY_MB", "-5") | ||
| if got := getMaxBodySize(); got != 10*1024*1024 { | ||
| t.Fatalf("expected 10MB fallback for negative, got %d", got) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("overflow clamped to max", func(t *testing.T) { | ||
| t.Setenv("MAX_REQUEST_BODY_MB", "99999") | ||
| if got := getMaxBodySize(); got != 100*1024*1024 { | ||
| t.Fatalf("expected 100MB clamped max, got %d", got) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("non-numeric falls back to default", func(t *testing.T) { | ||
| t.Setenv("MAX_REQUEST_BODY_MB", "not-a-number") | ||
| if got := getMaxBodySize(); got != 10*1024*1024 { | ||
| t.Fatalf("expected 10MB fallback for non-numeric, got %d", got) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| func TestRequestBodyLimit_EnforcedAtRouteLevel(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| envMB string | ||
| bodySize int | ||
| wantStatus int | ||
| wantMax string | ||
| }{ | ||
| { | ||
| name: "body below limit succeeds", | ||
| envMB: "10", | ||
| bodySize: 1024, | ||
| wantStatus: 200, | ||
| wantMax: "", | ||
| }, | ||
| { | ||
| name: "body just under limit succeeds", | ||
| envMB: "1", | ||
| bodySize: 1024*1024 - 1, | ||
| wantStatus: 200, | ||
| wantMax: "", | ||
| }, | ||
| { | ||
| name: "body at limit succeeds", | ||
| envMB: "1", | ||
| bodySize: 1024 * 1024, | ||
| wantStatus: 200, | ||
| wantMax: "", | ||
| }, | ||
| { | ||
| name: "body exceeds limit returns 413", | ||
| envMB: "1", | ||
| bodySize: 2 * 1024 * 1024, | ||
| wantStatus: 413, | ||
| wantMax: "1MB", | ||
| }, | ||
| { | ||
| name: "body way over limit returns 413", | ||
| envMB: "2", | ||
| bodySize: 10 * 1024 * 1024, | ||
| wantStatus: 413, | ||
| wantMax: "2MB", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| t.Setenv("MAX_REQUEST_BODY_MB", tt.envMB) | ||
| gin.SetMode(gin.TestMode) | ||
| r := gin.Default() | ||
| r.POST("/test", func(c *gin.Context) { | ||
| maxSize := getMaxBodySize() | ||
| c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize) | ||
| _, err := io.ReadAll(c.Request.Body) | ||
| if err != nil { | ||
| var maxBytesErr *http.MaxBytesError | ||
| if errors.As(err, &maxBytesErr) { | ||
| c.JSON(413, gin.H{ | ||
| "error": "Payload too large", | ||
| "max_size": fmt.Sprintf("%dMB", maxSize/1024/1024), | ||
| }) | ||
| return | ||
| } | ||
| c.JSON(500, gin.H{"error": err.Error()}) | ||
| return | ||
| } | ||
| c.JSON(200, gin.H{"ok": true}) | ||
| }) | ||
|
|
||
| body := bytes.Repeat([]byte("A"), tt.bodySize) | ||
| req, _ := http.NewRequest("POST", "/test", bytes.NewReader(body)) | ||
| w := httptest.NewRecorder() | ||
| r.ServeHTTP(w, req) | ||
|
|
||
| if w.Code != tt.wantStatus { | ||
| t.Fatalf("expected status %d, got %d", tt.wantStatus, w.Code) | ||
| } | ||
|
|
||
| if tt.wantMax != "" { | ||
| var resp map[string]interface{} | ||
| if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { | ||
| t.Fatalf("failed to parse response: %v", err) | ||
| } | ||
| if resp["max_size"] != tt.wantMax { | ||
| t.Errorf("expected max_size %q, got %v", tt.wantMax, resp["max_size"]) | ||
| } | ||
| } | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This introduces a new operator-facing gateway setting, but only
.env.examplementions it; the main README configuration table,gateway/README.mdoptional-variable table, and.env.production.examplestill omit it. In production/setup contexts users will not discover the new limit knob or know its default/unit, so please keep those config references aligned with this new env var.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@PranavAgarkar07