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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# Every field below maps 1:1 to a field on app.core.config.Config.

# --- Application ---
MAX_REQUEST_BODY_MB=50
MAX_REQUEST_BODY_MB=10
LOG_LEVEL=INFO
# One JSON analytics event per datastore action / dump request.
# false leaves the analytics middleware unmounted.
Expand Down
53 changes: 37 additions & 16 deletions datastore/api/middleware.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
from __future__ import annotations

import orjson
from starlette.types import ASGIApp, Receive, Scope, Send


class BodySizeLimitMiddleware:
"""Reject requests whose `Content-Length` exceeds `max_bytes` with 413.

Streaming bodies that omit `Content-Length` are not inspected here; cap
those at the ASGI server (uvicorn `--limit-max-request-size`) or behind
an ingress layer.

Streaming bodies that omit `Content-Length` are not inspected here;
cap those at the ASGI server (uvicorn `--limit-max-request-size`)
or behind an ingress layer.
"""

def __init__(self, app: ASGIApp, max_bytes: int) -> None:
Expand All @@ -24,19 +26,38 @@ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
except ValueError:
break
if size > self.max_bytes:
await send(
{
"type": "http.response.start",
"status": 413,
"headers": [(b"content-type", b"application/json")],
}
)
await send(
{
"type": "http.response.body",
"body": b'{"detail":"request body too large"}',
}
)
await self._reject(scope, send, size)
return
break
await self.app(scope, receive, send)

async def _reject(self, scope: Scope, send: Send, size: int) -> None:
limit_mb = self.max_bytes / (1024 * 1024)
size_mb = size / (1024 * 1024)
path = scope.get("path", "")
body = orjson.dumps(
{
"help": path,
"success": False,
"error": {
"__type": "Payload Too Large",
"message": (
f"Request body is {size_mb:.2f} MB; limit is "
f"{limit_mb:.0f} MB. Single write "
"at 10 MB — split the records into smaller "
"batches and retry."
),
},
}
)
await send(
{
"type": "http.response.start",
"status": 413,
"headers": [
(b"content-type", b"application/json"),
(b"content-length", str(len(body)).encode()),
],
}
)
await send({"type": "http.response.body", "body": body})
2 changes: 1 addition & 1 deletion datastore/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class Config(BaseSettings):

# Request limits
MAX_REQUEST_BODY_MB: int = Field(
default=50,
default=10,
ge=1,
le=1000,
description="Maximum request body size in MB",
Expand Down
Loading