Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions quantara/frontend/quantara.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
server {
listen 80;
client_max_body_size 1M;

server_name localhost;

Expand Down
1 change: 1 addition & 0 deletions quantara/frontend/quantara_dev.conf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ upstream backend_app {
server {
listen 443 ssl; # HTTPS
server_name quantara.xyz; # your domain
client_max_body_size 1M;

# SSL Certificates
ssl_certificate quantara.xyz.chain.crt; # Bundle file
Expand Down
2 changes: 2 additions & 0 deletions quantara/web_app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from web_app.api.referal import router as referal_router
from web_app.api.wallet_auth import router as auth_router
from web_app.config_validator import assert_valid_config
from web_app.api.middleware import MaxBodySizeMiddleware
from web_app.db.database import init_db
from web_app.db.database import init_db, get_database

Expand Down Expand Up @@ -139,6 +140,7 @@ async def global_exception_handler(request: Request, exc: Exception):
# Rate limiting middleware -- must be added after CORS/session so it wraps the
# full middleware stack and can reject requests before they reach routers.
app.add_middleware(SlowAPIMiddleware)
app.add_middleware(MaxBodySizeMiddleware, max_body_size=1024*1024)


@app.get("/health", tags=["Health"], summary="Health check endpoint")
Expand Down
69 changes: 69 additions & 0 deletions quantara/web_app/api/middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import json

class MaxBodySizeMiddleware:
"""ASGI middleware to enforce a maximum request body size.

Returns a 413 Payload Too Large response if the client request body exceeds
the configured maximum size (default 1MB).
"""

def __init__(self, app, max_body_size: int = 1024 * 1024):
self.app = app
self.max_body_size = max_body_size

async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return

# Check content-length header if present
content_length = 0
for name, value in scope.get("headers", []):
if name.lower() == b"content-length":
try:
content_length = int(value)
except ValueError:
pass
break

if content_length > self.max_body_size:
await self._send_413(send)
return

# Wrap receive to dynamically count body bytes read
total_received = 0
response_sent = False

async def custom_receive():
nonlocal total_received, response_sent
message = await receive()
if message["type"] == "http.request":
body_chunk = message.get("body", b"")
total_received += len(body_chunk)
if total_received > self.max_body_size:
if not response_sent:
await self._send_413(send)
response_sent = True
return {"type": "http.disconnect"}
return message

try:
await self.app(scope, custom_receive, send)
except Exception:
if response_sent:
return
raise

async def _send_413(self, send):
await send({
"type": "http.response.start",
"status": 413,
"headers": [
(b"content-type", b"application/json"),
]
})
await send({
"type": "http.response.body",
"body": json.dumps({"detail": "Request payload too large"}).encode("utf-8"),
"more_body": False
})
32 changes: 32 additions & 0 deletions quantara/web_app/tests/test_body_size_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest
from httpx import AsyncClient
from web_app.api.main import app

@pytest.mark.asyncio
async def test_body_size_limit_exceeds_1mb():
# Generate a payload slightly larger than 1MB (1,048,577 bytes)
payload = b"a" * (1024 * 1024 + 1)
async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.post("/api/vault/deposit", content=payload)
assert response.status_code == 413

@pytest.mark.asyncio
async def test_body_size_limit_within_1mb():
# Generate a payload within 1MB
payload = b"a" * 100
async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.post("/api/vault/deposit", content=payload)
# The response code should not be 413. Since it is /api/vault/deposit and doesn't
# have correct headers/payload, it might return 400, 401, or 422, but NOT 413.
assert response.status_code != 413

@pytest.mark.asyncio
async def test_body_size_limit_chunked_exceeds_1mb():
# Stream payload chunked, total exceeding 1MB
async def chunk_generator():
yield b"a" * (512 * 1024)
yield b"a" * (512 * 1024 + 1)

async with AsyncClient(app=app, base_url="http://testserver") as client:
response = await client.post("/api/vault/deposit", content=chunk_generator())
assert response.status_code == 413
Loading