Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ RATE_LIMIT_VERIFIED_RPM=120
# Cleanup interval for stale buckets (seconds)
RATE_LIMIT_CLEANUP_INTERVAL=300

# Request Body Size Configuration
# Maximum request body size in MB (default: 10)
MAX_REQUEST_BODY_MB=10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document MAX_REQUEST_BODY_MB in config references

This introduces a new operator-facing gateway setting, but only .env.example mentions it; the main README configuration table, gateway/README.md optional-variable table, and .env.production.example still 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 👍 / 👎.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


# Request Timeout Configuration
# Global request timeout (seconds)
REQUEST_TIMEOUT_SECONDS=60
Expand Down
2 changes: 2 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ SIGNATURE_EXPIRY_SECONDS=300
SIGNATURE_CLOCK_SKEW_SECONDS=60
RECEIPT_STORE=redis
CACHE_ENABLED=false
# Maximum request body size in MB (default: 10, max: 10240).
# MAX_REQUEST_BODY_MB=10
REQUEST_TIMEOUT_SECONDS=60
AI_REQUEST_TIMEOUT_SECONDS=30
# VERIFIER_TIMEOUT_SECONDS at the 2s default fails against Render free-tier
Expand Down
1 change: 1 addition & 0 deletions gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Common optional variables:
| `RECEIPT_TTL` | `86400` | Receipt TTL in seconds. |
| `CACHE_ENABLED` | `false` | Optional response cache. |
| `CACHE_TTL_SECONDS` | `3600` | Response cache TTL. |
| `MAX_REQUEST_BODY_MB` | `10` | Maximum request body size in MB (clamped to 10240). |
| `METRICS_ENABLED` | enabled unless set to `false` | Exposes Prometheus metrics. |
| `METRICS_PATH` | `/metrics` | Gateway metrics endpoint path. Missing leading slash is normalized. |
| `RATE_LIMIT_ENABLED` | disabled unless set to `true` or `1` | Enables token bucket middleware. |
Expand Down
10 changes: 5 additions & 5 deletions gateway/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,26 @@ func CacheMiddleware() gin.HandlerFunc {

// Read request body to generate cache key
// Check Content-Length first to reject oversized requests immediately
const maxBodySize = 10 * 1024 * 1024
maxSize := getMaxBodySize()
// ContentLength == -1 means unknown (chunked encoding or no header), proceed to MaxBytesReader
if c.Request.ContentLength > maxBodySize {
if c.Request.ContentLength > maxSize {
c.Header("Connection", "close")
c.JSON(413, gin.H{"error": "Payload too large", "max_size": "10MB"})
c.JSON(413, gin.H{"error": "Payload too large", "max_size": fmt.Sprintf("%dMB", maxSize/1024/1024)})
c.Abort()
return
}

var requestBody []byte
var err error
if c.Request.Body != nil {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, int64(maxBodySize))
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
requestBody, err = io.ReadAll(c.Request.Body)
if err != nil {
// If body too large, MaxBytesReader returns error
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
c.Header("Connection", "close")
c.JSON(413, gin.H{"error": "Payload too large", "max_size": "10MB"})
c.JSON(413, gin.H{"error": "Payload too large", "max_size": fmt.Sprintf("%dMB", maxSize/1024/1024)})
c.Abort()
return
}
Expand Down
19 changes: 19 additions & 0 deletions gateway/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,22 @@ func getVerifierTimeout() time.Duration { return getPositiveTimeout("VERIFIER_TI
func getHealthCheckTimeout() time.Duration {
return getPositiveTimeout("HEALTH_CHECK_TIMEOUT_SECONDS", 2)
}

const maxBodySizeMB = 10
const maxBodySizeMBMax = 10240 // 10 GB — safe upper bound to prevent int64 overflow
Comment thread
AnkanMisra marked this conversation as resolved.
Outdated

// getMaxBodySize returns the maximum request body size in bytes, configured via
// the MAX_REQUEST_BODY_MB environment variable. Defaults to 10MB. Clamped to
// maxBodySizeMBMax to prevent int64 overflow when converting to bytes.
func getMaxBodySize() int64 {
mb := getEnvAsInt("MAX_REQUEST_BODY_MB", maxBodySizeMB)
switch {
case mb <= 0:
log.Printf("Warning: MAX_REQUEST_BODY_MB must be positive, using default %d", maxBodySizeMB)
mb = maxBodySizeMB
case mb > maxBodySizeMBMax:
log.Printf("Warning: MAX_REQUEST_BODY_MB (%d) exceeds maximum %d, clamping", mb, maxBodySizeMBMax)
mb = maxBodySizeMBMax
}
return int64(mb) * 1024 * 1024
Comment thread
AnkanMisra marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
44 changes: 44 additions & 0 deletions gateway/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,3 +474,47 @@ 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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add route-level regression coverage for configured 413 responses

These added tests only validate getMaxBodySize() parsing, but they never exercise handleSummarize or CacheMiddleware with MAX_REQUEST_BODY_MB set to a smaller value and an oversized request body

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 != 10240*1024*1024 {
t.Fatalf("expected 10240MB 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)
}
})
}
6 changes: 3 additions & 3 deletions gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,13 +463,13 @@ func handleSummarize(c *gin.Context) {
// Read body if not already available
if requestBody == nil {
// Read body with limit (only if middleware didn't process it)
const maxBodySize = 10 * 1024 * 1024
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, int64(maxBodySize))
maxSize := getMaxBodySize()
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
requestBody, 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": "10MB"})
c.JSON(413, gin.H{"error": "Payload too large", "max_size": fmt.Sprintf("%dMB", maxSize/1024/1024)})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} else {
respondError(c, 500, "request_body_read_failed", err)
}
Expand Down
Loading
Loading