gabriel / musehub public
content_size.py python
95 lines 3.5 KB
Raw
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
1 """ASGI middleware: reject requests whose body exceeds the configured size limit.
2
3 Defence-in-depth: nginx already enforces ``client_max_body_size 500m`` at the
4 transport layer. This middleware adds a second check at the ASGI layer so that
5 requests that bypass nginx (local dev, direct container access) are also capped.
6
7 Size limits:
8 Push endpoints (/{owner}/{slug}/push/mpack-presign|push/unpack-mpack): 500 MB — matches nginx
9 All other routes : 10 MB
10
11 Two-phase check:
12 1. Content-Length header — fast rejection before reading any body bytes.
13 2. Streaming body counter — rejects mid-stream when Content-Length is absent.
14 On overflow a disconnect signal is sent to the handler and the response is
15 replaced with 413 Request Entity Too Large.
16 """
17
18 import re
19
20 from starlette.types import ASGIApp, Message, Receive, Scope, Send
21
22 # Push endpoints — large payloads expected (mpack presign + unpack).
23 _PUSH_PATH_RE = re.compile(r"^/[^/]+/[^/]+/push/(mpack-presign|unpack-mpack)$")
24
25 PUSH_MAX_BYTES: int = 500 * 1024 * 1024 # 500 MB — matches nginx client_max_body_size
26 API_MAX_BYTES: int = 10 * 1024 * 1024 # 10 MB — all other routes
27
28 _413_START = {
29 "type": "http.response.start",
30 "status": 413,
31 "headers": [(b"content-type", b"application/json")],
32 }
33 _413_BODY = {
34 "type": "http.response.body",
35 "body": b'{"detail":"Request body too large."}',
36 "more_body": False,
37 }
38
39 class ContentSizeLimitMiddleware:
40 """Pure-ASGI content-size limiter — no buffering of valid requests."""
41
42 def __init__(self, app: ASGIApp) -> None:
43 self.app = app
44
45 async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
46 if scope["type"] != "http":
47 await self.app(scope, receive, send)
48 return
49
50 path: str = scope.get("path", "")
51 limit = PUSH_MAX_BYTES if _PUSH_PATH_RE.match(path) else API_MAX_BYTES
52
53 # Phase 1: Content-Length header (fast path — no body reads needed).
54 for name, value in scope.get("headers", ()):
55 if name == b"content-length":
56 try:
57 if int(value) > limit:
58 await send(_413_START)
59 await send(_413_BODY)
60 return
61 except ValueError:
62 pass
63 break
64
65 # Phase 2: streaming body counter (covers chunked-transfer / no Content-Length).
66 total: int = 0
67 overflow: bool = False
68
69 async def _limited_receive() -> Message:
70 nonlocal total, overflow
71 if overflow:
72 return {"type": "http.disconnect"}
73 message = await receive()
74 if message.get("type") == "http.request":
75 total += len(message.get("body", b""))
76 if total > limit:
77 overflow = True
78 return {"type": "http.disconnect"}
79 return message
80
81 response_started: bool = False
82
83 async def _guarded_send(message: Message) -> None:
84 nonlocal response_started
85 if overflow and not response_started:
86 if message.get("type") == "http.response.start":
87 response_started = True
88 await send(_413_START)
89 await send(_413_BODY)
90 return
91 # Swallow body/trailer frames that follow an overridden start.
92 return
93 await send(message)
94
95 await self.app(scope, _limited_receive, _guarded_send)
File History 10 commits
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 30 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 35 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 36 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 36 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago