Что такое multipart/form-data
Что проверяют
Знаете ли вы, как физически устроен запрос с файлом, а не только аргумент клиента, который его собирает. За вопросом обычно идёт практика: почему загрузка ломается, откуда берутся лимиты и как не съесть память на большом файле.
Как отвечать
Это формат тела HTTP-запроса для форм, где кроме текстовых полей есть двоичные данные. Тело делится на части строкой-разделителем, её значение объявлено в заголовке Content-Type: multipart/form-data; boundary=.... У каждой части свои заголовки: обязательный Content-Disposition: form-data с именем поля и, для файла, с параметром filename, плюс собственный Content-Type. Разделитель в теле пишется с двумя ведущими дефисами, последний закрывается ещё двумя.
Главное отличие от application/x-www-form-urlencoded: там тело это одна процентно-кодированная строка, двоичные данные в ней распухают, а типа у отдельного поля нет. Multipart передаёт байты как есть и позволяет смешивать текст и файлы в одном запросе. JSON выбирают, когда файлов нет вовсе или когда файл уходит отдельным запросом.
На стороне Python разбор берут на себя фреймворки: request.files и request.form, request.FILES, а в FastAPI тип UploadFile, для которого нужен пакет python-multipart. Клиент requests собирает тело сам при аргументе files.
Где спотыкаются
Проставляют заголовок Content-Type руками. Клиент генерирует разделитель сам, а записанный вручную заголовок приходит без него: сервер отвечает ошибкой разбора, и виноватой назначают библиотеку.
Читают загруженный файл целиком в память. На паре гигабайт процесс умирает, поэтому файл читают кусками и сразу пишут в хранилище, а ограничение размера выставляют и в приложении, и на обратном прокси, иначе тело успевает доехать до кода.
Доверяют полю filename. Это строка от клиента: там встречаются пути, служебные символы и произвольная кодировка. Имя на диске генерируют сами, а присланное хранят как метаданные.