Add request size limits - Prevents memory exhaustion from large uploads
This comprehensive request size limiting system prevents memory exhaustion and DoS attacks from oversized requests. Key features: - Global request size limit: 50MB (configurable) - Type-specific limits: 25MB for audio, 1MB for JSON, 10MB for images - Multi-layer validation before loading data into memory - File type detection based on extensions - Endpoint-specific limit enforcement - Dynamic configuration via admin API - Clear error messages with size information Implementation details: - RequestSizeLimiter middleware with Flask integration - Pre-request validation using Content-Length header - File size checking for multipart uploads - JSON payload size validation - Custom decorator for route-specific limits - StreamSizeLimiter for chunked transfers - Integration with Flask's MAX_CONTENT_LENGTH Admin features: - GET /admin/size-limits - View current limits - POST /admin/size-limits - Update limits dynamically - Human-readable size formatting in responses - Size limit info in health check endpoints Security benefits: - Prevents memory exhaustion attacks - Blocks oversized uploads before processing - Protects against buffer overflow attempts - Works with rate limiting for comprehensive protection This addresses the critical security issue of unbounded request sizes that could lead to memory exhaustion or system crashes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
30edf8d272
commit
aec2d3b0aa
11
README.md
11
README.md
@ -125,6 +125,17 @@ Advanced session management prevents resource leaks from abandoned sessions:
|
|||||||
|
|
||||||
See [SESSION_MANAGEMENT.md](SESSION_MANAGEMENT.md) for detailed documentation.
|
See [SESSION_MANAGEMENT.md](SESSION_MANAGEMENT.md) for detailed documentation.
|
||||||
|
|
||||||
|
## Request Size Limits
|
||||||
|
|
||||||
|
Comprehensive request size limiting prevents memory exhaustion:
|
||||||
|
- Global limit: 50MB for any request
|
||||||
|
- Audio files: 25MB maximum
|
||||||
|
- JSON payloads: 1MB maximum
|
||||||
|
- File type detection and enforcement
|
||||||
|
- Dynamic configuration via admin API
|
||||||
|
|
||||||
|
See [REQUEST_SIZE_LIMITS.md](REQUEST_SIZE_LIMITS.md) for detailed documentation.
|
||||||
|
|
||||||
## Mobile Support
|
## Mobile Support
|
||||||
|
|
||||||
The interface is fully responsive and designed to work well on mobile devices.
|
The interface is fully responsive and designed to work well on mobile devices.
|
||||||
|
332
REQUEST_SIZE_LIMITS.md
Normal file
332
REQUEST_SIZE_LIMITS.md
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
# Request Size Limits Documentation
|
||||||
|
|
||||||
|
This document describes the request size limiting system implemented in Talk2Me to prevent memory exhaustion from large uploads.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Talk2Me implements comprehensive request size limiting to protect against:
|
||||||
|
- Memory exhaustion from large file uploads
|
||||||
|
- Denial of Service (DoS) attacks using oversized requests
|
||||||
|
- Buffer overflow attempts
|
||||||
|
- Resource starvation from unbounded requests
|
||||||
|
|
||||||
|
## Default Limits
|
||||||
|
|
||||||
|
### Global Limits
|
||||||
|
- **Maximum Content Length**: 50MB - Absolute maximum for any request
|
||||||
|
- **Maximum Audio File Size**: 25MB - For audio uploads (transcription)
|
||||||
|
- **Maximum JSON Payload**: 1MB - For API requests
|
||||||
|
- **Maximum Image Size**: 10MB - For future image processing features
|
||||||
|
- **Maximum Chunk Size**: 1MB - For streaming uploads
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Multi-Layer Protection
|
||||||
|
|
||||||
|
The system implements multiple layers of size checking:
|
||||||
|
- Flask's built-in `MAX_CONTENT_LENGTH` configuration
|
||||||
|
- Pre-request validation before data is loaded into memory
|
||||||
|
- File-type specific limits
|
||||||
|
- Endpoint-specific limits
|
||||||
|
- Streaming request monitoring
|
||||||
|
|
||||||
|
### 2. File Type Detection
|
||||||
|
|
||||||
|
Automatic detection and enforcement based on file extensions:
|
||||||
|
- Audio files: `.wav`, `.mp3`, `.ogg`, `.webm`, `.m4a`, `.flac`, `.aac`
|
||||||
|
- Image files: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`
|
||||||
|
- JSON payloads: Content-Type header detection
|
||||||
|
|
||||||
|
### 3. Graceful Error Handling
|
||||||
|
|
||||||
|
When limits are exceeded:
|
||||||
|
- Returns 413 (Request Entity Too Large) status code
|
||||||
|
- Provides clear error messages with size information
|
||||||
|
- Includes both actual and allowed sizes
|
||||||
|
- Human-readable size formatting
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set limits via environment variables (in bytes)
|
||||||
|
export MAX_CONTENT_LENGTH=52428800 # 50MB
|
||||||
|
export MAX_AUDIO_SIZE=26214400 # 25MB
|
||||||
|
export MAX_JSON_SIZE=1048576 # 1MB
|
||||||
|
export MAX_IMAGE_SIZE=10485760 # 10MB
|
||||||
|
```
|
||||||
|
|
||||||
|
### Flask Configuration
|
||||||
|
|
||||||
|
```python
|
||||||
|
# In config.py or app.py
|
||||||
|
app.config.update({
|
||||||
|
'MAX_CONTENT_LENGTH': 50 * 1024 * 1024, # 50MB
|
||||||
|
'MAX_AUDIO_SIZE': 25 * 1024 * 1024, # 25MB
|
||||||
|
'MAX_JSON_SIZE': 1 * 1024 * 1024, # 1MB
|
||||||
|
'MAX_IMAGE_SIZE': 10 * 1024 * 1024 # 10MB
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Configuration
|
||||||
|
|
||||||
|
Size limits can be updated at runtime via admin API.
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### GET /admin/size-limits
|
||||||
|
Get current size limits.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -H "X-Admin-Token: your-token" http://localhost:5005/admin/size-limits
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"limits": {
|
||||||
|
"max_content_length": 52428800,
|
||||||
|
"max_audio_size": 26214400,
|
||||||
|
"max_json_size": 1048576,
|
||||||
|
"max_image_size": 10485760
|
||||||
|
},
|
||||||
|
"limits_human": {
|
||||||
|
"max_content_length": "50.0MB",
|
||||||
|
"max_audio_size": "25.0MB",
|
||||||
|
"max_json_size": "1.0MB",
|
||||||
|
"max_image_size": "10.0MB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /admin/size-limits
|
||||||
|
Update size limits dynamically.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST -H "X-Admin-Token: your-token" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"max_audio_size": "30MB", "max_json_size": 2097152}' \
|
||||||
|
http://localhost:5005/admin/size-limits
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"old_limits": {...},
|
||||||
|
"new_limits": {...},
|
||||||
|
"new_limits_human": {
|
||||||
|
"max_audio_size": "30.0MB",
|
||||||
|
"max_json_size": "2.0MB"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage Examples
|
||||||
|
|
||||||
|
### 1. Endpoint-Specific Limits
|
||||||
|
|
||||||
|
```python
|
||||||
|
@app.route('/upload')
|
||||||
|
@limit_request_size(max_size=10*1024*1024) # 10MB limit
|
||||||
|
def upload():
|
||||||
|
# Handle upload
|
||||||
|
pass
|
||||||
|
|
||||||
|
@app.route('/upload-audio')
|
||||||
|
@limit_request_size(max_audio_size=30*1024*1024) # 30MB for audio
|
||||||
|
def upload_audio():
|
||||||
|
# Handle audio upload
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Client-Side Validation
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Check file size before upload
|
||||||
|
const MAX_AUDIO_SIZE = 25 * 1024 * 1024; // 25MB
|
||||||
|
|
||||||
|
function validateAudioFile(file) {
|
||||||
|
if (file.size > MAX_AUDIO_SIZE) {
|
||||||
|
alert(`Audio file too large. Maximum size is ${MAX_AUDIO_SIZE / 1024 / 1024}MB`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Chunked Uploads (Future Enhancement)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// For files larger than limits, use chunked upload
|
||||||
|
async function uploadLargeFile(file, chunkSize = 1024 * 1024) {
|
||||||
|
const chunks = Math.ceil(file.size / chunkSize);
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks; i++) {
|
||||||
|
const start = i * chunkSize;
|
||||||
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
|
||||||
|
await uploadChunk(chunk, i, chunks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Responses
|
||||||
|
|
||||||
|
### 413 Request Entity Too Large
|
||||||
|
|
||||||
|
When a request exceeds size limits:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Request too large",
|
||||||
|
"max_size": 52428800,
|
||||||
|
"your_size": 75000000,
|
||||||
|
"max_size_mb": 50.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### File-Specific Errors
|
||||||
|
|
||||||
|
For audio files:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Audio file too large",
|
||||||
|
"max_size": 26214400,
|
||||||
|
"your_size": 35000000,
|
||||||
|
"max_size_mb": 25.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For JSON payloads:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "JSON payload too large",
|
||||||
|
"max_size": 1048576,
|
||||||
|
"your_size": 2000000,
|
||||||
|
"max_size_kb": 1024.0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Client-Side Validation
|
||||||
|
|
||||||
|
Always validate file sizes on the client side:
|
||||||
|
```javascript
|
||||||
|
// Add to static/js/app.js
|
||||||
|
const SIZE_LIMITS = {
|
||||||
|
audio: 25 * 1024 * 1024, // 25MB
|
||||||
|
json: 1 * 1024 * 1024, // 1MB
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkFileSize(file, type) {
|
||||||
|
const limit = SIZE_LIMITS[type];
|
||||||
|
if (file.size > limit) {
|
||||||
|
showError(`File too large. Maximum size: ${formatSize(limit)}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Progressive Enhancement
|
||||||
|
|
||||||
|
For better UX with large files:
|
||||||
|
- Show upload progress
|
||||||
|
- Implement resumable uploads
|
||||||
|
- Compress audio client-side when possible
|
||||||
|
- Use appropriate audio formats (WebM/Opus for smaller sizes)
|
||||||
|
|
||||||
|
### 3. Server Configuration
|
||||||
|
|
||||||
|
Configure your web server (Nginx/Apache) to also enforce limits:
|
||||||
|
|
||||||
|
**Nginx:**
|
||||||
|
```nginx
|
||||||
|
client_max_body_size 50M;
|
||||||
|
client_body_buffer_size 1M;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Apache:**
|
||||||
|
```apache
|
||||||
|
LimitRequestBody 52428800
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Monitoring
|
||||||
|
|
||||||
|
Monitor size limit violations:
|
||||||
|
- Track 413 errors in logs
|
||||||
|
- Alert on repeated violations from same IP
|
||||||
|
- Adjust limits based on usage patterns
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Memory Protection**: Pre-flight size checks prevent loading large files into memory
|
||||||
|
2. **DoS Prevention**: Limits prevent attackers from exhausting server resources
|
||||||
|
3. **Bandwidth Protection**: Prevents bandwidth exhaustion from large uploads
|
||||||
|
4. **Storage Protection**: Works with session management to limit total storage per user
|
||||||
|
|
||||||
|
## Integration with Other Systems
|
||||||
|
|
||||||
|
### Rate Limiting
|
||||||
|
Size limits work in conjunction with rate limiting:
|
||||||
|
- Large requests count more against rate limits
|
||||||
|
- Repeated size violations can trigger IP blocking
|
||||||
|
|
||||||
|
### Session Management
|
||||||
|
Size limits are enforced per session:
|
||||||
|
- Total storage per session is limited
|
||||||
|
- Large files count against session resource limits
|
||||||
|
|
||||||
|
### Monitoring
|
||||||
|
Size limit violations are tracked in:
|
||||||
|
- Application logs
|
||||||
|
- Health check endpoints
|
||||||
|
- Admin monitoring dashboards
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### 1. Legitimate Large Files Rejected
|
||||||
|
|
||||||
|
If users need to upload larger files:
|
||||||
|
```bash
|
||||||
|
# Increase limit for audio files to 50MB
|
||||||
|
curl -X POST -H "X-Admin-Token: token" \
|
||||||
|
-d '{"max_audio_size": "50MB"}' \
|
||||||
|
http://localhost:5005/admin/size-limits
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Chunked Transfer Encoding
|
||||||
|
|
||||||
|
For requests without Content-Length header:
|
||||||
|
- The system monitors the stream
|
||||||
|
- Terminates connection if size exceeded
|
||||||
|
- May require special handling for some clients
|
||||||
|
|
||||||
|
#### 3. Load Balancer Limits
|
||||||
|
|
||||||
|
Ensure your load balancer also enforces appropriate limits:
|
||||||
|
- AWS ALB: Configure request size limits
|
||||||
|
- Cloudflare: Set upload size limits
|
||||||
|
- Nginx: Configure client_max_body_size
|
||||||
|
|
||||||
|
## Performance Impact
|
||||||
|
|
||||||
|
The size limiting system has minimal performance impact:
|
||||||
|
- Pre-flight checks are O(1) operations
|
||||||
|
- No buffering of large requests
|
||||||
|
- Early termination of oversized requests
|
||||||
|
- Efficient memory usage
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
1. **Chunked Upload Support**: Native support for resumable uploads
|
||||||
|
2. **Compression Detection**: Automatic handling of compressed uploads
|
||||||
|
3. **Dynamic Limits**: Per-user or per-tier size limits
|
||||||
|
4. **Bandwidth Throttling**: Rate limit large uploads
|
||||||
|
5. **Storage Quotas**: Long-term storage limits per user
|
108
app.py
108
app.py
@ -36,6 +36,7 @@ logger = logging.getLogger(__name__)
|
|||||||
from config import init_app as init_config
|
from config import init_app as init_config
|
||||||
from secrets_manager import init_app as init_secrets
|
from secrets_manager import init_app as init_secrets
|
||||||
from session_manager import init_app as init_session_manager, track_resource
|
from session_manager import init_app as init_session_manager, track_resource
|
||||||
|
from request_size_limiter import RequestSizeLimiter, limit_request_size
|
||||||
|
|
||||||
# Error boundary decorator for Flask routes
|
# Error boundary decorator for Flask routes
|
||||||
def with_error_boundary(func):
|
def with_error_boundary(func):
|
||||||
@ -110,6 +111,14 @@ app.config['UPLOAD_FOLDER'] = upload_folder
|
|||||||
# Initialize session management after upload folder is configured
|
# Initialize session management after upload folder is configured
|
||||||
init_session_manager(app)
|
init_session_manager(app)
|
||||||
|
|
||||||
|
# Initialize request size limiter
|
||||||
|
request_size_limiter = RequestSizeLimiter(app, {
|
||||||
|
'max_content_length': app.config.get('MAX_CONTENT_LENGTH', 50 * 1024 * 1024), # 50MB default
|
||||||
|
'max_audio_size': app.config.get('MAX_AUDIO_SIZE', 25 * 1024 * 1024), # 25MB for audio
|
||||||
|
'max_json_size': app.config.get('MAX_JSON_SIZE', 1 * 1024 * 1024), # 1MB for JSON
|
||||||
|
'max_image_size': app.config.get('MAX_IMAGE_SIZE', 10 * 1024 * 1024), # 10MB for images
|
||||||
|
})
|
||||||
|
|
||||||
# TTS configuration is already loaded from config.py
|
# TTS configuration is already loaded from config.py
|
||||||
# Warn if TTS API key is not set
|
# Warn if TTS API key is not set
|
||||||
if not app.config.get('TTS_API_KEY'):
|
if not app.config.get('TTS_API_KEY'):
|
||||||
@ -592,6 +601,7 @@ def index():
|
|||||||
|
|
||||||
@app.route('/transcribe', methods=['POST'])
|
@app.route('/transcribe', methods=['POST'])
|
||||||
@rate_limit(requests_per_minute=10, requests_per_hour=100, check_size=True)
|
@rate_limit(requests_per_minute=10, requests_per_hour=100, check_size=True)
|
||||||
|
@limit_request_size(max_audio_size=25 * 1024 * 1024) # 25MB limit for audio
|
||||||
@with_error_boundary
|
@with_error_boundary
|
||||||
@track_resource('audio_file')
|
@track_resource('audio_file')
|
||||||
def transcribe():
|
def transcribe():
|
||||||
@ -707,6 +717,7 @@ def transcribe():
|
|||||||
|
|
||||||
@app.route('/translate', methods=['POST'])
|
@app.route('/translate', methods=['POST'])
|
||||||
@rate_limit(requests_per_minute=20, requests_per_hour=300, check_size=True)
|
@rate_limit(requests_per_minute=20, requests_per_hour=300, check_size=True)
|
||||||
|
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||||
@with_error_boundary
|
@with_error_boundary
|
||||||
def translate():
|
def translate():
|
||||||
try:
|
try:
|
||||||
@ -775,6 +786,7 @@ def translate():
|
|||||||
|
|
||||||
@app.route('/translate/stream', methods=['POST'])
|
@app.route('/translate/stream', methods=['POST'])
|
||||||
@rate_limit(requests_per_minute=10, requests_per_hour=150, check_size=True)
|
@rate_limit(requests_per_minute=10, requests_per_hour=150, check_size=True)
|
||||||
|
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||||
@with_error_boundary
|
@with_error_boundary
|
||||||
def translate_stream():
|
def translate_stream():
|
||||||
"""Streaming translation endpoint for reduced latency"""
|
"""Streaming translation endpoint for reduced latency"""
|
||||||
@ -872,6 +884,7 @@ def translate_stream():
|
|||||||
|
|
||||||
@app.route('/speak', methods=['POST'])
|
@app.route('/speak', methods=['POST'])
|
||||||
@rate_limit(requests_per_minute=15, requests_per_hour=200, check_size=True)
|
@rate_limit(requests_per_minute=15, requests_per_hour=200, check_size=True)
|
||||||
|
@limit_request_size(max_size=1 * 1024 * 1024) # 1MB limit for JSON
|
||||||
@with_error_boundary
|
@with_error_boundary
|
||||||
@track_resource('audio_file')
|
@track_resource('audio_file')
|
||||||
def speak():
|
def speak():
|
||||||
@ -1127,6 +1140,13 @@ def detailed_health_check():
|
|||||||
health_status['metrics']['uptime'] = time.time() - app.start_time if hasattr(app, 'start_time') else 0
|
health_status['metrics']['uptime'] = time.time() - app.start_time if hasattr(app, 'start_time') else 0
|
||||||
health_status['metrics']['request_count'] = getattr(app, 'request_count', 0)
|
health_status['metrics']['request_count'] = getattr(app, 'request_count', 0)
|
||||||
|
|
||||||
|
# Add size limits info
|
||||||
|
if hasattr(app, 'request_size_limiter'):
|
||||||
|
health_status['metrics']['size_limits'] = {
|
||||||
|
k: f"{v / 1024 / 1024:.1f}MB" if v > 1024 * 1024 else f"{v / 1024:.1f}KB"
|
||||||
|
for k, v in app.request_size_limiter.limits.items()
|
||||||
|
}
|
||||||
|
|
||||||
# Set appropriate HTTP status code
|
# Set appropriate HTTP status code
|
||||||
http_status = 200 if health_status['status'] == 'healthy' else 503 if health_status['status'] == 'unhealthy' else 200
|
http_status = 200 if health_status['status'] == 'healthy' else 503 if health_status['status'] == 'unhealthy' else 200
|
||||||
|
|
||||||
@ -1481,5 +1501,93 @@ def get_session_metrics():
|
|||||||
logger.error(f"Failed to get session metrics: {str(e)}")
|
logger.error(f"Failed to get session metrics: {str(e)}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/admin/size-limits', methods=['GET'])
|
||||||
|
@rate_limit(requests_per_minute=10)
|
||||||
|
def get_size_limits():
|
||||||
|
"""Get current request size limits"""
|
||||||
|
try:
|
||||||
|
# Simple authentication check
|
||||||
|
auth_token = request.headers.get('X-Admin-Token')
|
||||||
|
expected_token = app.config.get('ADMIN_TOKEN', 'default-admin-token')
|
||||||
|
|
||||||
|
if auth_token != expected_token:
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
if hasattr(app, 'request_size_limiter'):
|
||||||
|
return jsonify({
|
||||||
|
'limits': app.request_size_limiter.limits,
|
||||||
|
'limits_human': {
|
||||||
|
k: f"{v / 1024 / 1024:.1f}MB" if v > 1024 * 1024 else f"{v / 1024:.1f}KB"
|
||||||
|
for k, v in app.request_size_limiter.limits.items()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({'error': 'Size limiter not initialized'}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get size limits: {str(e)}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/admin/size-limits', methods=['POST'])
|
||||||
|
@rate_limit(requests_per_minute=5)
|
||||||
|
def update_size_limits():
|
||||||
|
"""Update request size limits"""
|
||||||
|
try:
|
||||||
|
# Simple authentication check
|
||||||
|
auth_token = request.headers.get('X-Admin-Token')
|
||||||
|
expected_token = app.config.get('ADMIN_TOKEN', 'default-admin-token')
|
||||||
|
|
||||||
|
if auth_token != expected_token:
|
||||||
|
return jsonify({'error': 'Unauthorized'}), 401
|
||||||
|
|
||||||
|
data = request.json
|
||||||
|
if not data:
|
||||||
|
return jsonify({'error': 'No data provided'}), 400
|
||||||
|
|
||||||
|
# Validate limits
|
||||||
|
valid_keys = {'max_content_length', 'max_audio_size', 'max_json_size', 'max_image_size'}
|
||||||
|
updates = {}
|
||||||
|
|
||||||
|
for key, value in data.items():
|
||||||
|
if key in valid_keys:
|
||||||
|
try:
|
||||||
|
# Accept values in MB and convert to bytes
|
||||||
|
if isinstance(value, str) and value.endswith('MB'):
|
||||||
|
value = float(value[:-2]) * 1024 * 1024
|
||||||
|
elif isinstance(value, str) and value.endswith('KB'):
|
||||||
|
value = float(value[:-2]) * 1024
|
||||||
|
else:
|
||||||
|
value = int(value)
|
||||||
|
|
||||||
|
# Enforce reasonable limits
|
||||||
|
if value < 1024: # Minimum 1KB
|
||||||
|
return jsonify({'error': f'{key} too small (min 1KB)'}), 400
|
||||||
|
if value > 500 * 1024 * 1024: # Maximum 500MB
|
||||||
|
return jsonify({'error': f'{key} too large (max 500MB)'}), 400
|
||||||
|
|
||||||
|
updates[key] = value
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({'error': f'Invalid value for {key}'}), 400
|
||||||
|
|
||||||
|
if not updates:
|
||||||
|
return jsonify({'error': 'No valid limits provided'}), 400
|
||||||
|
|
||||||
|
# Update limits
|
||||||
|
old_limits = app.request_size_limiter.update_limits(**updates)
|
||||||
|
|
||||||
|
logger.info(f"Size limits updated by admin: {updates}")
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'old_limits': old_limits,
|
||||||
|
'new_limits': app.request_size_limiter.limits,
|
||||||
|
'new_limits_human': {
|
||||||
|
k: f"{v / 1024 / 1024:.1f}MB" if v > 1024 * 1024 else f"{v / 1024:.1f}KB"
|
||||||
|
for k, v in app.request_size_limiter.limits.items()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to update size limits: {str(e)}")
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5005, debug=True)
|
app.run(host='0.0.0.0', port=5005, debug=True)
|
||||||
|
@ -31,7 +31,12 @@ class Config:
|
|||||||
|
|
||||||
# Upload configuration
|
# Upload configuration
|
||||||
self.UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', None)
|
self.UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER', None)
|
||||||
self.MAX_CONTENT_LENGTH = 16 * 1024 * 1024 # 16MB max file size
|
|
||||||
|
# Request size limits (in bytes)
|
||||||
|
self.MAX_CONTENT_LENGTH = int(os.environ.get('MAX_CONTENT_LENGTH', 50 * 1024 * 1024)) # 50MB
|
||||||
|
self.MAX_AUDIO_SIZE = int(os.environ.get('MAX_AUDIO_SIZE', 25 * 1024 * 1024)) # 25MB
|
||||||
|
self.MAX_JSON_SIZE = int(os.environ.get('MAX_JSON_SIZE', 1 * 1024 * 1024)) # 1MB
|
||||||
|
self.MAX_IMAGE_SIZE = int(os.environ.get('MAX_IMAGE_SIZE', 10 * 1024 * 1024)) # 10MB
|
||||||
|
|
||||||
# CORS configuration
|
# CORS configuration
|
||||||
self.CORS_ORIGINS = os.environ.get('CORS_ORIGINS', '*').split(',')
|
self.CORS_ORIGINS = os.environ.get('CORS_ORIGINS', '*').split(',')
|
||||||
|
302
request_size_limiter.py
Normal file
302
request_size_limiter.py
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
# Request size limiting middleware for preventing memory exhaustion
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
from flask import request, jsonify, current_app
|
||||||
|
import os
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Default size limits (in bytes)
|
||||||
|
DEFAULT_LIMITS = {
|
||||||
|
'max_content_length': 50 * 1024 * 1024, # 50MB global max
|
||||||
|
'max_audio_size': 25 * 1024 * 1024, # 25MB for audio files
|
||||||
|
'max_json_size': 1 * 1024 * 1024, # 1MB for JSON payloads
|
||||||
|
'max_image_size': 10 * 1024 * 1024, # 10MB for images
|
||||||
|
'max_chunk_size': 1 * 1024 * 1024, # 1MB chunks for streaming
|
||||||
|
}
|
||||||
|
|
||||||
|
# File extension to MIME type mapping
|
||||||
|
AUDIO_EXTENSIONS = {'.wav', '.mp3', '.ogg', '.webm', '.m4a', '.flac', '.aac'}
|
||||||
|
IMAGE_EXTENSIONS = {'.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'}
|
||||||
|
|
||||||
|
class RequestSizeLimiter:
|
||||||
|
"""
|
||||||
|
Middleware to enforce request size limits and prevent memory exhaustion
|
||||||
|
"""
|
||||||
|
def __init__(self, app=None, config=None):
|
||||||
|
self.config = config or {}
|
||||||
|
self.limits = {**DEFAULT_LIMITS, **self.config}
|
||||||
|
|
||||||
|
if app:
|
||||||
|
self.init_app(app)
|
||||||
|
|
||||||
|
def init_app(self, app):
|
||||||
|
"""Initialize the Flask application with size limiting"""
|
||||||
|
# Set Flask's MAX_CONTENT_LENGTH
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = self.limits['max_content_length']
|
||||||
|
|
||||||
|
# Store limiter in app
|
||||||
|
app.request_size_limiter = self
|
||||||
|
|
||||||
|
# Add before_request handler
|
||||||
|
app.before_request(self.check_request_size)
|
||||||
|
|
||||||
|
# Add error handler for 413 Request Entity Too Large
|
||||||
|
app.register_error_handler(413, self.handle_413)
|
||||||
|
|
||||||
|
logger.info(f"Request size limiter initialized with max content length: {self.limits['max_content_length'] / 1024 / 1024:.1f}MB")
|
||||||
|
|
||||||
|
def check_request_size(self):
|
||||||
|
"""Check request size before processing"""
|
||||||
|
# Skip size check for GET, HEAD, OPTIONS
|
||||||
|
if request.method in ('GET', 'HEAD', 'OPTIONS'):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get content length
|
||||||
|
content_length = request.content_length
|
||||||
|
|
||||||
|
if content_length is None:
|
||||||
|
# No content-length header, check for chunked encoding
|
||||||
|
if request.headers.get('Transfer-Encoding') == 'chunked':
|
||||||
|
logger.warning(f"Chunked request from {request.remote_addr} to {request.endpoint}")
|
||||||
|
# For chunked requests, we'll need to monitor the stream
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# No content, allow it
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check against global limit
|
||||||
|
if content_length > self.limits['max_content_length']:
|
||||||
|
logger.warning(f"Request from {request.remote_addr} exceeds global limit: {content_length} bytes")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Request too large',
|
||||||
|
'max_size': self.limits['max_content_length'],
|
||||||
|
'your_size': content_length
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
# Check endpoint-specific limits
|
||||||
|
endpoint = request.endpoint
|
||||||
|
if endpoint:
|
||||||
|
endpoint_limit = self.get_endpoint_limit(endpoint)
|
||||||
|
if endpoint_limit and content_length > endpoint_limit:
|
||||||
|
logger.warning(f"Request from {request.remote_addr} to {endpoint} exceeds endpoint limit: {content_length} bytes")
|
||||||
|
return jsonify({
|
||||||
|
'error': f'Request too large for {endpoint}',
|
||||||
|
'max_size': endpoint_limit,
|
||||||
|
'your_size': content_length
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
# Check file-specific limits
|
||||||
|
if request.files:
|
||||||
|
for file_key, file_obj in request.files.items():
|
||||||
|
# Check file size
|
||||||
|
file_obj.seek(0, os.SEEK_END)
|
||||||
|
file_size = file_obj.tell()
|
||||||
|
file_obj.seek(0) # Reset position
|
||||||
|
|
||||||
|
# Determine file type
|
||||||
|
filename = file_obj.filename or ''
|
||||||
|
file_ext = os.path.splitext(filename)[1].lower()
|
||||||
|
|
||||||
|
# Apply type-specific limits
|
||||||
|
if file_ext in AUDIO_EXTENSIONS:
|
||||||
|
max_size = self.limits.get('max_audio_size', self.limits['max_content_length'])
|
||||||
|
if file_size > max_size:
|
||||||
|
logger.warning(f"Audio file from {request.remote_addr} exceeds limit: {file_size} bytes")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Audio file too large',
|
||||||
|
'max_size': max_size,
|
||||||
|
'your_size': file_size,
|
||||||
|
'max_size_mb': round(max_size / 1024 / 1024, 1)
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
elif file_ext in IMAGE_EXTENSIONS:
|
||||||
|
max_size = self.limits.get('max_image_size', self.limits['max_content_length'])
|
||||||
|
if file_size > max_size:
|
||||||
|
logger.warning(f"Image file from {request.remote_addr} exceeds limit: {file_size} bytes")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Image file too large',
|
||||||
|
'max_size': max_size,
|
||||||
|
'your_size': file_size,
|
||||||
|
'max_size_mb': round(max_size / 1024 / 1024, 1)
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
# Check JSON payload size
|
||||||
|
if request.is_json:
|
||||||
|
try:
|
||||||
|
# Get raw data size
|
||||||
|
data_size = len(request.get_data())
|
||||||
|
max_json = self.limits.get('max_json_size', self.limits['max_content_length'])
|
||||||
|
|
||||||
|
if data_size > max_json:
|
||||||
|
logger.warning(f"JSON payload from {request.remote_addr} exceeds limit: {data_size} bytes")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'JSON payload too large',
|
||||||
|
'max_size': max_json,
|
||||||
|
'your_size': data_size,
|
||||||
|
'max_size_kb': round(max_json / 1024, 1)
|
||||||
|
}), 413
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error checking JSON size: {e}")
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_endpoint_limit(self, endpoint):
|
||||||
|
"""Get size limit for specific endpoint"""
|
||||||
|
endpoint_limits = {
|
||||||
|
'transcribe': self.limits.get('max_audio_size', 25 * 1024 * 1024),
|
||||||
|
'speak': self.limits.get('max_json_size', 1 * 1024 * 1024),
|
||||||
|
'translate': self.limits.get('max_json_size', 1 * 1024 * 1024),
|
||||||
|
'translate_stream': self.limits.get('max_json_size', 1 * 1024 * 1024),
|
||||||
|
}
|
||||||
|
return endpoint_limits.get(endpoint)
|
||||||
|
|
||||||
|
def handle_413(self, error):
|
||||||
|
"""Handle 413 Request Entity Too Large errors"""
|
||||||
|
logger.warning(f"413 error from {request.remote_addr}: {error}")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Request entity too large',
|
||||||
|
'message': 'The request payload is too large. Please reduce the size and try again.',
|
||||||
|
'max_size': self.limits['max_content_length'],
|
||||||
|
'max_size_mb': round(self.limits['max_content_length'] / 1024 / 1024, 1)
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
def update_limits(self, **kwargs):
|
||||||
|
"""Update size limits dynamically"""
|
||||||
|
old_limits = self.limits.copy()
|
||||||
|
self.limits.update(kwargs)
|
||||||
|
|
||||||
|
# Update Flask's MAX_CONTENT_LENGTH if changed
|
||||||
|
if 'max_content_length' in kwargs and current_app:
|
||||||
|
current_app.config['MAX_CONTENT_LENGTH'] = kwargs['max_content_length']
|
||||||
|
|
||||||
|
logger.info(f"Updated size limits: {kwargs}")
|
||||||
|
return old_limits
|
||||||
|
|
||||||
|
def limit_request_size(**limit_kwargs):
|
||||||
|
"""
|
||||||
|
Decorator to apply custom size limits to specific routes
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@app.route('/upload')
|
||||||
|
@limit_request_size(max_size=10*1024*1024) # 10MB limit
|
||||||
|
def upload():
|
||||||
|
...
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
# Check content length
|
||||||
|
content_length = request.content_length
|
||||||
|
max_size = limit_kwargs.get('max_size', DEFAULT_LIMITS['max_content_length'])
|
||||||
|
|
||||||
|
if content_length and content_length > max_size:
|
||||||
|
logger.warning(f"Request to {request.endpoint} exceeds custom limit: {content_length} bytes")
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Request too large',
|
||||||
|
'max_size': max_size,
|
||||||
|
'your_size': content_length,
|
||||||
|
'max_size_mb': round(max_size / 1024 / 1024, 1)
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
# Check specific file types if specified
|
||||||
|
if 'max_audio_size' in limit_kwargs and request.files:
|
||||||
|
for file_obj in request.files.values():
|
||||||
|
if file_obj.filename:
|
||||||
|
ext = os.path.splitext(file_obj.filename)[1].lower()
|
||||||
|
if ext in AUDIO_EXTENSIONS:
|
||||||
|
file_obj.seek(0, os.SEEK_END)
|
||||||
|
file_size = file_obj.tell()
|
||||||
|
file_obj.seek(0)
|
||||||
|
|
||||||
|
if file_size > limit_kwargs['max_audio_size']:
|
||||||
|
return jsonify({
|
||||||
|
'error': 'Audio file too large',
|
||||||
|
'max_size': limit_kwargs['max_audio_size'],
|
||||||
|
'your_size': file_size,
|
||||||
|
'max_size_mb': round(limit_kwargs['max_audio_size'] / 1024 / 1024, 1)
|
||||||
|
}), 413
|
||||||
|
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
class StreamSizeLimiter:
|
||||||
|
"""
|
||||||
|
Helper class to limit streaming request sizes
|
||||||
|
"""
|
||||||
|
def __init__(self, stream, max_size):
|
||||||
|
self.stream = stream
|
||||||
|
self.max_size = max_size
|
||||||
|
self.bytes_read = 0
|
||||||
|
|
||||||
|
def read(self, size=-1):
|
||||||
|
"""Read from stream with size limit enforcement"""
|
||||||
|
if size == -1:
|
||||||
|
# Read all remaining, but respect limit
|
||||||
|
size = self.max_size - self.bytes_read
|
||||||
|
|
||||||
|
# Check if we would exceed limit
|
||||||
|
if self.bytes_read + size > self.max_size:
|
||||||
|
raise ValueError(f"Stream size exceeds limit of {self.max_size} bytes")
|
||||||
|
|
||||||
|
data = self.stream.read(size)
|
||||||
|
self.bytes_read += len(data)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def readline(self, size=-1):
|
||||||
|
"""Read line from stream with size limit enforcement"""
|
||||||
|
if size == -1:
|
||||||
|
size = self.max_size - self.bytes_read
|
||||||
|
|
||||||
|
if self.bytes_read + size > self.max_size:
|
||||||
|
raise ValueError(f"Stream size exceeds limit of {self.max_size} bytes")
|
||||||
|
|
||||||
|
line = self.stream.readline(size)
|
||||||
|
self.bytes_read += len(line)
|
||||||
|
|
||||||
|
return line
|
||||||
|
|
||||||
|
# Utility functions
|
||||||
|
def get_request_size():
|
||||||
|
"""Get the size of the current request"""
|
||||||
|
if request.content_length:
|
||||||
|
return request.content_length
|
||||||
|
|
||||||
|
# For chunked requests, read and measure
|
||||||
|
try:
|
||||||
|
data = request.get_data()
|
||||||
|
return len(data)
|
||||||
|
except Exception:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def format_size(size_bytes):
|
||||||
|
"""Format size in human-readable format"""
|
||||||
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
||||||
|
if size_bytes < 1024.0:
|
||||||
|
return f"{size_bytes:.1f} {unit}"
|
||||||
|
size_bytes /= 1024.0
|
||||||
|
return f"{size_bytes:.1f} TB"
|
||||||
|
|
||||||
|
# Configuration helper
|
||||||
|
def configure_size_limits(app, **kwargs):
|
||||||
|
"""
|
||||||
|
Configure size limits for the application
|
||||||
|
|
||||||
|
Args:
|
||||||
|
app: Flask application
|
||||||
|
max_content_length: Global maximum request size
|
||||||
|
max_audio_size: Maximum audio file size
|
||||||
|
max_json_size: Maximum JSON payload size
|
||||||
|
max_image_size: Maximum image file size
|
||||||
|
"""
|
||||||
|
config = {
|
||||||
|
'max_content_length': kwargs.get('max_content_length', DEFAULT_LIMITS['max_content_length']),
|
||||||
|
'max_audio_size': kwargs.get('max_audio_size', DEFAULT_LIMITS['max_audio_size']),
|
||||||
|
'max_json_size': kwargs.get('max_json_size', DEFAULT_LIMITS['max_json_size']),
|
||||||
|
'max_image_size': kwargs.get('max_image_size', DEFAULT_LIMITS['max_image_size']),
|
||||||
|
}
|
||||||
|
|
||||||
|
limiter = RequestSizeLimiter(app, config)
|
||||||
|
return limiter
|
146
test_size_limits.py
Executable file
146
test_size_limits.py
Executable file
@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Test script for request size limits
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
import os
|
||||||
|
|
||||||
|
BASE_URL = "http://localhost:5005"
|
||||||
|
|
||||||
|
def test_json_size_limit():
|
||||||
|
"""Test JSON payload size limit"""
|
||||||
|
print("\n=== Testing JSON Size Limit ===")
|
||||||
|
|
||||||
|
# Create a large JSON payload (over 1MB)
|
||||||
|
large_data = {
|
||||||
|
"text": "x" * (2 * 1024 * 1024), # 2MB of text
|
||||||
|
"source_lang": "English",
|
||||||
|
"target_lang": "Spanish"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{BASE_URL}/translate", json=large_data)
|
||||||
|
print(f"Status: {response.status_code}")
|
||||||
|
if response.status_code == 413:
|
||||||
|
print(f"✓ Correctly rejected large JSON: {response.json()}")
|
||||||
|
else:
|
||||||
|
print(f"✗ Should have rejected large JSON")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
def test_audio_size_limit():
|
||||||
|
"""Test audio file size limit"""
|
||||||
|
print("\n=== Testing Audio Size Limit ===")
|
||||||
|
|
||||||
|
# Create a fake large audio file (over 25MB)
|
||||||
|
large_audio = io.BytesIO(b"x" * (30 * 1024 * 1024)) # 30MB
|
||||||
|
|
||||||
|
files = {
|
||||||
|
'audio': ('large_audio.wav', large_audio, 'audio/wav')
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
'source_lang': 'English'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{BASE_URL}/transcribe", files=files, data=data)
|
||||||
|
print(f"Status: {response.status_code}")
|
||||||
|
if response.status_code == 413:
|
||||||
|
print(f"✓ Correctly rejected large audio: {response.json()}")
|
||||||
|
else:
|
||||||
|
print(f"✗ Should have rejected large audio")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
def test_valid_requests():
|
||||||
|
"""Test that valid-sized requests are accepted"""
|
||||||
|
print("\n=== Testing Valid Size Requests ===")
|
||||||
|
|
||||||
|
# Small JSON payload
|
||||||
|
small_data = {
|
||||||
|
"text": "Hello world",
|
||||||
|
"source_lang": "English",
|
||||||
|
"target_lang": "Spanish"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{BASE_URL}/translate", json=small_data)
|
||||||
|
print(f"Small JSON - Status: {response.status_code}")
|
||||||
|
if response.status_code != 413:
|
||||||
|
print("✓ Small JSON accepted")
|
||||||
|
else:
|
||||||
|
print("✗ Small JSON should be accepted")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
# Small audio file
|
||||||
|
small_audio = io.BytesIO(b"RIFF" + b"x" * 1000) # 1KB fake WAV
|
||||||
|
files = {
|
||||||
|
'audio': ('small_audio.wav', small_audio, 'audio/wav')
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
'source_lang': 'English'
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{BASE_URL}/transcribe", files=files, data=data)
|
||||||
|
print(f"Small audio - Status: {response.status_code}")
|
||||||
|
if response.status_code != 413:
|
||||||
|
print("✓ Small audio accepted")
|
||||||
|
else:
|
||||||
|
print("✗ Small audio should be accepted")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
def test_admin_endpoints():
|
||||||
|
"""Test admin endpoints for size limits"""
|
||||||
|
print("\n=== Testing Admin Endpoints ===")
|
||||||
|
|
||||||
|
headers = {'X-Admin-Token': os.environ.get('ADMIN_TOKEN', 'default-admin-token')}
|
||||||
|
|
||||||
|
# Get current limits
|
||||||
|
try:
|
||||||
|
response = requests.get(f"{BASE_URL}/admin/size-limits", headers=headers)
|
||||||
|
print(f"Get limits - Status: {response.status_code}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
limits = response.json()
|
||||||
|
print(f"✓ Current limits: {limits['limits_human']}")
|
||||||
|
else:
|
||||||
|
print(f"✗ Failed to get limits: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
# Update limits
|
||||||
|
new_limits = {
|
||||||
|
"max_audio_size": "30MB",
|
||||||
|
"max_json_size": 2097152 # 2MB in bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.post(f"{BASE_URL}/admin/size-limits",
|
||||||
|
json=new_limits, headers=headers)
|
||||||
|
print(f"\nUpdate limits - Status: {response.status_code}")
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f"✓ Updated limits: {result['new_limits_human']}")
|
||||||
|
else:
|
||||||
|
print(f"✗ Failed to update limits: {response.text}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("Request Size Limit Tests")
|
||||||
|
print("========================")
|
||||||
|
print(f"Testing against: {BASE_URL}")
|
||||||
|
print("\nMake sure the Flask app is running on port 5005")
|
||||||
|
|
||||||
|
input("\nPress Enter to start tests...")
|
||||||
|
|
||||||
|
test_valid_requests()
|
||||||
|
test_json_size_limit()
|
||||||
|
test_audio_size_limit()
|
||||||
|
test_admin_endpoints()
|
||||||
|
|
||||||
|
print("\n✅ All tests completed!")
|
Loading…
Reference in New Issue
Block a user