Request Limits#

Fail fast on requests that are too large or too slow. This middleware guards your app (and upstream services) by enforcing body size caps, upload timeouts, and optional header limits before the handler runs.


What it does#

  • Rejects requests whose Content-Length exceeds a configured maxBodyBytes

  • Aborts requests that take longer than maxUploadSeconds to arrive (slowloris-style drips)

  • Optionally caps header size/count to avoid header abuse

  • Returns clear error responses (e.g., 413 Payload Too Large, 408 Request Timeout) with retry guidance

  • Plays nicely with throttling and caching middleware (run early)


Wiring#

Place in pre-global before anything expensive:

$preGlobal = [
  \Infocyph\Webrick\Middleware\RequestLimitsMiddleware::class,
  // throttle, cookies, negotiation, validators...
];

If you need custom limits:

$preGlobal[] = new \Infocyph\Webrick\Middleware\RequestLimitsMiddleware(
    maxBodyBytes: 10 * 1024 * 1024,   // 10 MiB
    maxUploadSeconds: 30,             // timeout while reading body
    maxHeaderBytes: 32 * 1024,        // optional
    maxHeaderCount: 100                // optional
);

(Adjust constructor names to your implementation.)


Typical limits#

Context

maxBodyBytes

maxUploadSeconds

Notes

Public JSON APIs

1–2 MiB

15–30s

Keep tight to block abuse

Auth/Login

128–256 KiB

10–20s

Small forms; protect brute-force

File Uploads

handled elsewhere

n/a

Prefer dedicated upload endpoints with explicit checks

Admin/Internal APIs

5–10 MiB

30–60s

Depends on use cases


Behavior & status codes#

  • Too big: if Content-Length (or measured bytes) exceeds maxBodyBytes413 Payload Too Large

    • Response may include Retry-After or a JSON error with guidance

  • Too slow: if body doesn’t arrive in time → 408 Request Timeout (or 400 depending on policy)

  • Header abuse: exceed header limits → 400 Bad Request with an explanatory code

Prefer rejecting before reading entire bodies to save CPU and bandwidth.


Working with proxies & servers#

  • Nginx/Apache limits (e.g., client_max_body_size, LimitRequestBody) should be aligned with or slightly stricter than app limits for early drops at the edge.

  • If your reverse proxy buffers uploads, the app may see the payload all at once—app limits still help as a second line.

  • Ensure timeout settings (proxy/connect/read) are compatible with maxUploadSeconds.



Observability#

Expose counters/timers:

  • requests_limited_total{reason="body_too_large"}

  • requests_limited_total{reason="timeout"}

  • request_body_bytes{route="..."} (histogram)

Optional: emit Server-Timing: request-limits;dur=… for investigations.


Error format (example)#

{
  "error": {
    "code": "E_BODY_TOO_LARGE",
    "message": "Payload exceeds 1048576 bytes",
    "limit": 1048576
  }
}

Standardize across your API so client libraries can respond intelligently.


Configuration#

use Infocyph\Webrick\Middleware\RequestLimitsMiddleware;

$preGlobal[] = new RequestLimitsMiddleware(
    maxHeaderBytes: 8192,                           // Max total header size
    maxHeaderCount: 100,                            // Max number of header fields
    maxBodyBytes: null,                             // Defaults to ini_get('post_max_size')
    bodyLimitVerbs: ['POST', 'PUT', 'PATCH', 'DELETE'],
    violateOnUnknownBody: true                      // Reject if no Content-Length
);

Constructor Parameters#

Parameter

Type

Default

Description

maxHeaderBytes

int

8192

Maximum total size of all headers

maxHeaderCount

int

100

Maximum number of header fields

maxBodyBytes

int|null

post_max_size from ini

Maximum request body size

bodyLimitVerbs

array<string>

['POST', 'PUT', ...]

Methods to apply body size limit

violateOnUnknownBody

bool

true

Reject chunked/unknown size bodies


What Gets Checked#

1. Header Field Count (431 Payload Too Large)#

Counts each header name-value pair:

Content-Type: application/json   // Count: 1
Accept: text/html                // Count: 2
Accept: application/json         // Count: 3 (if sent twice)
X-Custom: value                  // Count: 4

Why limit: Prevents header-based DoS attacks.

2. Header Byte Size (431 Request Header Fields Too Large)#

Sums the byte length of all Name: value pairs:

Content-Type: application/json   // ~31 bytes
Authorization: Bearer eyJ...     // ~200 bytes (JWT)

Why limit: Prevents memory exhaustion from huge headers.

3. Body Size (413 Payload Too Large)#

Based on Content-Length header:

POST /upload
Content-Length: 10485760  // 10MB

Body: [10MB of data]

Why limit: Prevents disk/memory exhaustion, protects upload handlers.

Important: Does NOT pre-reject Transfer-Encoding: chunked (can’t know size upfront).

4. Transfer-Encoding Handling#

// Known transfer codings (bypassed for pre-check):
- "identity"   // No transformation
- "trailers"   // HTTP/2 legitimate use

// Actual codings (allow through, can't know size):
- "chunked"
- "compress", "deflate", "gzip" (rare in requests)

If Transfer-Encoding: chunked, middleware allows the request (validates later if needed).


HTTP/2 Safety#

Never emits Connection: close for HTTP/2:

// Only for HTTP/1.x
if (str_starts_with($protocol, 'HTTP/1.')) {
    return $response->withHeader('Connection', 'close');
}

HTTP/2 doesn’t use Connection header; middleware respects this.


Typical Limits by Use Case#

Use Case

Headers

Header Bytes

Body Size

Public API

50

4 KiB

1 MiB

JSON API

50

8 KiB

2 MiB

File Uploads

50

8 KiB

100 MiB

Auth/Login

30

2 KiB

256 KiB

Admin Interface

100

16 KiB

10 MiB

Webhooks

50

4 KiB

5 MiB


Examples#

Strict Limits (Public API)#

new RequestLimitsMiddleware(
    maxHeaderBytes: 4096,       // 4KB headers
    maxHeaderCount: 50,
    maxBodyBytes: 1_048_576,    // 1MB body
    bodyLimitVerbs: ['POST', 'PUT', 'PATCH', 'DELETE']
);

Relaxed Limits (File Upload Endpoint)#

// Use different limits per route
Route::post('/upload', [UploadController::class, 'store'], [
    'middleware' => [
        new RequestLimitsMiddleware(
            maxBodyBytes: 104_857_600  // 100MB for uploads
        )
    ]
]);

Conservative (High Security)#

new RequestLimitsMiddleware(
    maxHeaderBytes: 2048,       // 2KB
    maxHeaderCount: 30,
    maxBodyBytes: 262_144,      // 256KB
    violateOnUnknownBody: true  // Reject chunked
);

Error Responses#

413 Payload Too Large#

HTTP/1.1 413 Payload Too Large
Content-Type: application/json

{
  "error": {
    "code": "E_BODY_TOO_LARGE",
    "message": "Request body exceeds 1048576 bytes",
    "limit": 1048576,
    "received": 2097152
  }
}

431 Request Header Fields Too Large#

HTTP/1.1 431 Request Header Fields Too Large

{
  "error": {
    "code": "E_HEADERS_TOO_LARGE",
    "message": "Total header size exceeds 8192 bytes",
    "limit": 8192,
    "received": 10240
  }
}

Interaction with Web Server Limits#

Nginx#

Align limits:

# nginx.conf
client_max_body_size 10M;       # Should be >= maxBodyBytes
client_header_buffer_size 4k;
large_client_header_buffers 4 8k;  # Max 32KB total

Recommendation: Make Nginx limits slightly stricter to fail fast at the edge.

Apache#

# httpd.conf
LimitRequestBody 10485760        # 10MB (should be >= maxBodyBytes)
LimitRequestFields 50            # Max header count
LimitRequestFieldSize 8190       # Max single header size
LimitRequestLine 8190            # Max request line size

PHP INI#

post_max_size = 10M
upload_max_filesize = 10M
max_input_vars = 1000

Chain: Nginx/Apache → PHP INI → Middleware

Each layer should be compatible (upstream stricter or equal).


Bypass for Specific Routes#

// Global limits
$preGlobal[] = new RequestLimitsMiddleware(maxBodyBytes: 1_048_576);

// Override for file upload
Route::post('/files/upload', [FileController::class, 'upload'], [
    'middleware' => [
        new RequestLimitsMiddleware(maxBodyBytes: 104_857_600)  // 100MB
    ]
]);

Important: Per-route middleware runs after pre-global, so set global to highest needed or skip global for body limits.


Security Considerations#

Slowloris Protection#

Limit request duration, not just size:

// In conjunction with web server timeouts
new RequestLimitsMiddleware(
    maxBodyBytes: 10_485_760,
    // Web server should also set:
    // - Nginx: client_body_timeout 30s
    // - Apache: Timeout 30
);

Memory Exhaustion#

Even with limits, buffering large requests consumes memory:

// Prefer streaming/chunked processing for large uploads
// Use temporary files, not memory buffering
move_uploaded_file($file->getTmpName(), $destination);

Header Injection#

Limit header count to prevent:

  • Header injection attacks

  • Cache poisoning via many Vary headers

maxHeaderCount: 50  // Reasonable for most apps

Monitoring#

Metrics to Track#

request_limits_violations_total{reason="body_too_large"}
request_limits_violations_total{reason="headers_too_large"}
request_limits_violations_total{reason="too_many_headers"}
request_body_size_bytes (histogram)
request_header_size_bytes (histogram)

Logging#

// In middleware or wrapper
if ($bodyTooLarge) {
    $logger->warning('Request body too large', [
        'path' => $request->getPath(),
        'ip' => $request->getAttribute('client_ip'),
        'content_length' => $request->getHeaderLine('Content-Length'),
        'limit' => $maxBodyBytes
    ]);
}

Troubleshooting#

Issue: Legitimate uploads rejected#

Symptoms: Users can’t upload valid files

Causes:

  1. maxBodyBytes too strict

  2. Web server limit lower than app limit

  3. Chunked encoding blocked

Debug:

# Check actual file size
ls -lh upload.jpg  # 5.2M

# Check Content-Length sent
curl -v -F "[email protected]" http://localhost/upload 2>&1 | grep Content-Length

# Test with explicit limit
curl -v -F "[email protected]" \
  --max-filesize 10485760 \
  http://localhost/upload

Fix:

// Increase limit for upload endpoint
Route::post('/upload', /* ... */, [
    'middleware' => [
        new RequestLimitsMiddleware(maxBodyBytes: 20_971_520)  // 20MB
    ]
]);

Issue: Headers rejected in production#

Symptoms: Requests work in dev, fail in prod with 431

Causes:

  1. Large JWTs in Authorization header

  2. Many cookies

  3. Verbose User-Agent strings

Debug:

// Log header sizes
$totalSize = 0;
foreach ($request->getHeaders() as $name => $values) {
    foreach ($values as $value) {
        $size = strlen("{$name}: {$value}\r\n");
        $totalSize += $size;
        error_log("{$name}: {$size} bytes");
    }
}
error_log("Total: {$totalSize} bytes");

Fix:

// Increase header limit
new RequestLimitsMiddleware(
    maxHeaderBytes: 16384  // 16KB (was 8KB)
);

Issue: Chunked requests fail#

Symptoms: Streaming uploads always return 413

Cause: violateOnUnknownBody: true blocks chunked encoding

Fix:

new RequestLimitsMiddleware(
    violateOnUnknownBody: false  // Allow chunked
);

Alternative: Validate size as data streams in, not upfront.


Best Practices#

Do#

  1. Set explicit limits

    maxBodyBytes: 10_485_760  // Explicit 10MB
    
  2. Align with upstream

    client_max_body_size 10M;  # Nginx
    
    maxBodyBytes: 10_485_760   # App
    
  3. Use different limits per route

    // Strict for API, relaxed for uploads
    Route::post('/api/*', middleware: [new RequestLimitsMiddleware(maxBodyBytes: 1_048_576)]);
    Route::post('/upload', middleware: [new RequestLimitsMiddleware(maxBodyBytes: 104_857_600)]);
    
  4. Monitor violations

    if ($violated) {
        $metrics->increment('request_limits_violations', ['reason' => $reason]);
    }
    
  5. Provide clear errors

    {
      "error": "Request too large",
      "limit": "10MB",
      "documentation": "https://docs.example.com/api-limits"
    }
    

Don’t#

  1. Don’t use unlimited

    maxBodyBytes: null  // ❌ Dangerous
    maxBodyBytes: PHP_INT_MAX  // ❌ Same
    
  2. Don’t block chunked unnecessarily

    violateOnUnknownBody: true  // ❌ Breaks streaming
    
  3. Don’t mismatch upstream/downstream

    client_max_body_size 1M;  # Nginx
    
    maxBodyBytes: 104_857_600  # 100MB (never reached!)
    
  4. Don’t ignore limits on internal endpoints

    // ❌ Even internal endpoints should have limits
    Route::post('/internal/webhook', /* no limits */);
    
    // ✅ Set reasonable limits
    Route::post('/internal/webhook', middleware: [
        new RequestLimitsMiddleware(maxBodyBytes: 10_485_760)
    ]);
    

Summary#

RequestLimitsMiddleware protects against:

  • ✅ Memory exhaustion (large bodies/headers)

  • ✅ Disk exhaustion (large uploads)

  • ✅ CPU exhaustion (parsing huge headers)

  • ✅ Application-level DoS

Key configuration:

  1. maxBodyBytes: Size per endpoint type (API vs uploads)

  2. maxHeaderBytes: Usually 8KB sufficient

  3. maxHeaderCount: Usually 50-100 sufficient

  4. Alignment: Upstream (Nginx) ≤ App limits

Golden rule: Set explicit, per-use-case limits and monitor violations.


Troubleshooting#

Symptom

Likely cause

Fix

Users hit 413 on normal forms

Limit too strict

Raise maxBodyBytes slightly; compress forms; remove unused fields

Random 408s on mobile

Aggressive timeout

Increase maxUploadSeconds; verify proxy timeouts

Limits ignored in production

Proxy terminating earlier

Align Nginx/Apache limits with app settings; document expectations

Large JSON blows memory

Full buffering

Prefer streaming parsers for huge payloads; re-design endpoints


Checklist#

  • Add RequestLimits early in pre-global

  • Set tight defaults for public APIs; per-endpoint overrides for uploads

  • Align proxy/web-server limits and timeouts

  • Emit clear error shapes and metrics

  • Load test with realistic slow clients to validate thresholds