Files
my-v0-server/app/core/body_limit.py
T

72 lines
2.4 KiB
Python

"""限制请求体大小:声明的 Content-Length 超限直接 413,没声明(分块)或声明得不实的,边读边数
FastAPI 会先把整个请求体收完再调用接口函数,超大的请求体已经占满了带宽和内存
和数据文件上传的 UploadSizeLimit 同一出发点;限制随路径而不同,所以用规则表,第一条命中的生效
"""
import re
from dataclasses import dataclass
from fastapi.responses import JSONResponse
from starlette.exceptions import HTTPException
from starlette.types import ASGIApp, Message, Receive, Scope, Send
from app.common.response import error_content
class BodyTooLarge(HTTPException):
"""边读边数时超限;message 由全局 HTTP 异常处理放进信封,和声明超限的响应文案一致"""
def __init__(self, message: str):
super().__init__(413)
self.message = message
@dataclass(frozen=True)
class BodyLimitRule:
methods: frozenset[str]
path: re.Pattern[str]
max_bytes: int
message: str
class BodyLimit:
def __init__(self, app: ASGIApp, rules: list[BodyLimitRule]):
self.app = app
self.rules = rules
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
rule = self._match(scope) if scope["type"] == "http" else None
if rule is None:
await self.app(scope, receive, send)
return
length = dict(scope["headers"]).get(b"content-length", b"")
if length.isdigit() and int(length) > rule.max_bytes:
response = JSONResponse(error_content(413, rule.message), 413)
await response(scope, receive, send)
return
received = 0
async def counted() -> Message:
nonlocal received
message = await receive()
if message["type"] == "http.request":
received += len(message.get("body", b""))
if received > rule.max_bytes:
# 抛出的异常由应用里统一的 HTTP 异常处理转成 413 信封
raise BodyTooLarge(rule.message)
return message
await self.app(scope, counted, send)
def _match(self, scope: Scope) -> BodyLimitRule | None:
return next(
(
rule
for rule in self.rules
if scope["method"] in rule.methods
and rule.path.fullmatch(scope["path"])
),
None,
)