Harden PDF uploads: size cap, magic-byte check, non-blocking parse

read_pdf_upload() bounds reads at 10 MB and requires the %PDF magic
before anything reaches pdftotext (SEC-06); rejections land in the
per-file errors list with the filename. Parsing now runs via
asyncio.to_thread so a slow PDF no longer blocks the event loop for up
to 30s (BE-14). Review notes: temp files were already context-managed
(BE-09 overstated) and the pension batch already commits atomically
(BE-10 overstated). 69 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Carlos Escalante
2026-06-10 15:00:49 -06:00
parent 3cdd7d9eab
commit 82a7bfc4c5
4 changed files with 77 additions and 4 deletions

View File

@@ -2,11 +2,14 @@ from datetime import datetime
from dateutil.relativedelta import relativedelta
from typing import Optional
import asyncio
from fastapi import APIRouter, Depends, Query, UploadFile
from pydantic import BaseModel
from sqlmodel import Session, select
from app.auth import get_current_user
from app.uploads import read_pdf_upload
from app.db import get_session
from app.models.models import (
Category,
@@ -204,8 +207,9 @@ async def upload_municipal_receipt(
errors: list[str] = []
try:
pdf_bytes = await file.read()
data = extract_municipal_receipt(pdf_bytes, filename)
pdf_bytes = await read_pdf_upload(file)
# pdftotext runs as a subprocess; keep the event loop free
data = await asyncio.to_thread(extract_municipal_receipt, pdf_bytes, filename)
except ValueError as e:
return MunicipalReceiptUploadResult(imported=0, updated=0, errors=[str(e)])
except Exception as e: