MediaRouter / app /core /request_limits.py
basyx's picture
Make request body limit middleware return reliable 413 responses
bda3e6b verified
Raw History Blame Contribute Delete
4.14 kB
from __future__ import annotations
from starlette.types import ASGIApp, Message, Receive, Scope, Send
class _RequestBodyTooLarge(Exception):
"""Internal signal used to stop an application from consuming an oversized body."""
class RequestBodyLimitMiddleware:
"""Enforce a request-body ceiling using Content-Length and streamed byte counts."""
def __init__(self, app: ASGIApp, max_bytes: int) -> None:
if max_bytes < 1:
raise ValueError("max_bytes must be positive")
self.app = app
self.max_bytes = max_bytes
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http" or scope["method"] in {"GET", "HEAD", "OPTIONS"}:
await self.app(scope, receive, send)
return
headers = {key.lower(): value for key, value in scope.get("headers", [])}
declared = headers.get(b"content-length")
if declared is not None:
try:
content_length = int(declared)
except ValueError:
await self._send_413(send)
return
if content_length < 0:
await self._send_413(send)
return
if content_length > self.max_bytes:
await self._send_413(send)
return
received = 0
exceeded = False
response_started = False
response_body_sent = False
async def limited_receive() -> Message:
nonlocal received, exceeded
message = await receive()
if message["type"] == "http.request":
received += len(message.get("body", b""))
if received > self.max_bytes:
exceeded = True
raise _RequestBodyTooLarge
return message
async def guarded_send(message: Message) -> None:
nonlocal response_started, response_body_sent
if not exceeded:
if message["type"] == "http.response.start":
response_started = True
await send(message)
return
# Replace any application response after overflow with one clean 413.
if message["type"] == "http.response.start" and not response_started:
response_started = True
body = b'{"detail":"Request body too large"}'
await send({
"type": "http.response.start",
"status": 413,
"headers": [
(b"content-type", b"application/json"),
(b"content-length", str(len(body)).encode("ascii")),
(b"cache-control", b"no-store"),
],
})
elif message["type"] == "http.response.body" and not response_body_sent:
response_body_sent = True
await send({
"type": "http.response.body",
"body": b'{"detail":"Request body too large"}',
"more_body": False,
})
try:
await self.app(scope, limited_receive, guarded_send)
except _RequestBodyTooLarge:
if not response_started:
await self._send_413(send)
elif not response_body_sent:
response_body_sent = True
await send({
"type": "http.response.body",
"body": b'{"detail":"Request body too large"}',
"more_body": False,
})
async def _send_413(self, send: Send) -> None:
body = b'{"detail":"Request body too large"}'
await send({
"type": "http.response.start",
"status": 413,
"headers": [
(b"content-type", b"application/json"),
(b"content-length", str(len(body)).encode("ascii")),
(b"cache-control", b"no-store"),
],
})
await send({"type": "http.response.body", "body": body, "more_body": False})