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 |
|
|
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) exceedsmaxBodyBytes→ 413 Payload Too LargeResponse may include
Retry-Afteror 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.
File uploads: recommended pattern#
Instead of high global limits, create a dedicated upload route with explicit checks:
Route::post('/upload', function ($r) {
$f = $r->file('avatar');
if (!$f || $f->getError()) {
return Response::json(['error'=>'invalid upload'], 400);
}
if ($f->getSize() > 2 * 1024 * 1024) {
return Response::json(['error'=>'file too large'], 413);
}
// validate mime/extension; move to storage
return Response::json(['ok'=>true], 201);
}, ['middleware' => ['throttle:5,60']]);
Keep global limits modest; scale per-endpoint where needed.
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 |
|---|---|---|---|
|
|
|
Maximum total size of all headers |
|
|
|
Maximum number of header fields |
|
|
|
Maximum request body size |
|
|
|
Methods to apply body size limit |
|
|
|
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
Varyheaders
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:
maxBodyBytestoo strictWeb server limit lower than app limit
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:
Large JWTs in
AuthorizationheaderMany cookies
Verbose
User-Agentstrings
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#
Set explicit limits
maxBodyBytes: 10_485_760 // Explicit 10MB
Align with upstream
client_max_body_size 10M; # Nginx
maxBodyBytes: 10_485_760 # App
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)]);
Monitor violations
if ($violated) { $metrics->increment('request_limits_violations', ['reason' => $reason]); }
Provide clear errors
{ "error": "Request too large", "limit": "10MB", "documentation": "https://docs.example.com/api-limits" }
❌ Don’t#
Don’t use unlimited
maxBodyBytes: null // ❌ Dangerous maxBodyBytes: PHP_INT_MAX // ❌ Same
Don’t block chunked unnecessarily
violateOnUnknownBody: true // ❌ Breaks streaming
Don’t mismatch upstream/downstream
client_max_body_size 1M; # Nginx
maxBodyBytes: 104_857_600 # 100MB (never reached!)
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:
maxBodyBytes: Size per endpoint type (API vs uploads)
maxHeaderBytes: Usually 8KB sufficient
maxHeaderCount: Usually 50-100 sufficient
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 |
Random 408s on mobile |
Aggressive timeout |
Increase |
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