A multipart request divides an HTTP body into named fields and file parts that need both parser-level and application-level limits.
Python Flask multipart uploads: bound the body, part count and accepted file
Operation contract
The owned endpoint requires exactly one receipt_id field and one receipt file. Flask limits the total body, parser field memory and part count before the endpoint validates the accepted names. The application reads at most 129 file bytes for a 128-byte limit and returns only a byte count. It never uses the received filename as a storage path. Three test requests check an accepted upload, an oversized file and a multipart body containing an extra part.
Failure and ownership boundary
Parser limits do not replace reverse-proxy limits or a real transport deadline. Reaching request.form or request.files can trigger parsing, so catching errors after partially saving files is too late for staged publication. Flask error responses: preserve status without exposing internal exception text, Python JSON Lines: cap line bytes and validate the whole batch before returning it and Python pathlib files: specify encoding and close the resource owner cover the adjacent boundaries. This endpoint uses a local test client and has no public identity or storage service.
Tested environment
Dependency check: this program was executed on CPython 3.14.6 with Flask==3.1.3. Install these versions in a separate virtual environment. The download includes the recorded environment snapshot; no third-party package is part of the website runtime.
Working program
import io
import re
from flask import Flask, request
app = Flask(__name__)
app.config.update(MAX_CONTENT_LENGTH=4096, MAX_FORM_MEMORY_SIZE=4096, MAX_FORM_PARTS=2)
@app.errorhandler(413)
def too_large(failure):
return {"error": "upload budget exceeded"}, 413
@app.post("/receipts")
def receive():
if set(request.form) != {"receipt_id"} or set(request.files) != {"receipt"} or len(request.form.getlist("receipt_id")) != 1 or len(request.files.getlist("receipt")) != 1:
return {"error": "exact parts required"}, 400
if re.fullmatch(r"R-[0-9]{4}", request.form["receipt_id"]) is None:
return {"error": "invalid receipt ID"}, 400
payload = request.files["receipt"].stream.read(129)
if len(payload) > 128:
return {"error": "file byte budget exceeded"}, 413
return {"bytes": len(payload)}, 201
with app.test_client() as client:
accepted = client.post("/receipts", data={"receipt_id": "R-0041", "receipt": (io.BytesIO(b"amount=125"), "received.txt")})
oversized = client.post("/receipts", data={"receipt_id": "R-0041", "receipt": (io.BytesIO(b"x" * 129), "received.txt")})
extra = client.post("/receipts", data={"receipt_id": "R-0041", "receipt": (io.BytesIO(b"ok"), "received.txt"), "extra": "field"})
print("accepted:", accepted.status_code, accepted.json)
print("file limit:", oversized.status_code)
print("part limit:", extra.status_code)Output
accepted: 201 {'bytes': 10}
file limit: 413
part limit: 413Costs and limits
The endpoint retains at most 129 file bytes; parser and transport buffers have separate configured and implementation costs. A received request may already occupy upstream storage. Upload retention, authentication, content inspection and crash-durable publication are not provided by returning a count.
Common Mistakes
- The received filename must not become an unchecked destination path.
- Limit multipart part count as well as individual application payload size.
Connected lessons
Flask error responses: preserve status without exposing internal exception text, Python ZIP extraction: validate names and decompressed budgets before owned writes, Python JSON Lines: cap line bytes and validate the whole batch before returning it.
Follow the ownership and update boundary
Python SpooledTemporaryFile: rollover is a storage choice, not an upload limit.
