"""Shared resource limits for multipart files materialized in memory.""" from __future__ import annotations from fastapi import UploadFile from app.core.exceptions import AppException async def read_upload_bounded( upload: UploadFile, max_bytes: int, *, code: str = "UPLOAD_TOO_LARGE", message: str | None = None, rewind: bool = False, close: bool = False, ) -> bytes: """Read at most ``max_bytes + 1`` and reject oversized files. Starlette may spool a multipart part to disk, but an unbounded ``UploadFile.read()`` materializes the entire part in process memory. The sentinel byte lets callers distinguish an exact-boundary file from an oversized one without ever allocating the attacker's complete payload. Oversized uploads are always closed immediately to release their spool. """ if max_bytes < 0: raise ValueError("max_bytes must be non-negative") if rewind and close: raise ValueError("rewind and close are mutually exclusive") try: payload = await upload.read(max_bytes + 1) if len(payload) > max_bytes: raise AppException( code=code, message=message or f"uploaded file exceeds {max_bytes} byte limit", status_code=413, ) if rewind: await upload.seek(0) except BaseException: # A failed read/rewind leaves the part unusable. Release the # SpooledTemporaryFile without masking the original exception. try: await upload.close() except Exception: # noqa: BLE001 -- preserve the primary failure pass raise if close: await upload.close() return payload