Заголовки, cookies и формы

Кроме пути, query и тела-JSON, FastAPI умеет извлекать данные из заголовков (Header), cookies (Cookie) и форм (Form) — каждым источником управляет свой маркер.

Источник данных вы выбираете явно: Annotated[str, Header()] читает заголовок, Cookie() — куку, Form() — поле формы. Без маркера простой тип читается как query.

HTTP-запрос несёт больше, чем путь и тело. Заголовки сообщают язык, тип контента, токен авторизации. Cookies хранят сессию. Формы (application/x-www-form-urlencoded или multipart/form-data) приходят со страниц с классическими HTML-формами и при загрузке файлов. FastAPI даёт по маркеру на каждый источник.

from fastapi import FastAPI, Header, Cookie, Form
from typing import Annotated

app = FastAPI()

@app.get("/info")
async def info(
    user_agent: Annotated[str | None, Header()] = None,
    session_id: Annotated[str | None, Cookie()] = None,
):
    return {"ua": user_agent, "session": session_id}

@app.post("/login")
async def login(
    username: Annotated[str, Form()],
    password: Annotated[str, Form()],
):
    return {"user": username}

Тонкость с заголовками: HTTP-заголовки нечувствительны к регистру и пишутся через дефис (User-Agent), а имена параметров Python — через подчёркивание. FastAPI автоматически сопоставляет user_agent с заголовком user-agent. Формы требуют, чтобы был установлен python-multipart, и не могут соседствовать с JSON-телом в одном запросе: запрос либо form-data, либо JSON.

Как работает под капотом

Заголовки — это просто пары «имя: значение», где сравнение имени регистронезависимо. Формы в кодировке x-www-form-urlencoded — это та же query-строка, только в теле. Смоделируем оба разбора на stdlib:

from urllib.parse import parse_qs

# заголовки приходят как сырой текст; имена регистронезависимы
raw_headers = "User-Agent: Mozilla/5.0
Accept-Language: ru-RU
Cookie: session_id=abc123"

headers = {}
cookies = {}
for line in raw_headers.split("
"):
    name, _, value = line.partition(":")
    name = name.strip().lower()           # нормализуем регистр
    value = value.strip()
    if name == "cookie":
        for pair in value.split(";"):
            k, _, v = pair.strip().partition("=")
            cookies[k] = v
    else:
        headers[name] = value

print("user-agent ->", headers.get("user-agent"))
print("session_id (cookie) ->", cookies.get("session_id"))

# form-data в кодировке x-www-form-urlencoded — это query в теле
form_body = "username=ernest&password=secret"
form = {k: v[0] for k, v in parse_qs(form_body).items()}
print("форма ->", form)

Попробуй сам ▶ Видно три источника сразу: нормализация заголовка по регистру, разбор cookie и разбор формы.

Частые ошибки

Первая — забыть установить python-multipart и получить непонятную ошибку при использовании Form. Вторая — пытаться одновременно принять Form и Pydantic-тело-JSON в одном обработчике; это разные форматы запроса. Третья — вручную писать имена заголовков с дефисами в имени Python-параметра (так нельзя) вместо подчёркиваний. Четвёртая — хранить чувствительные данные в обычных, не HttpOnly куках.

Best practices

  • Явно маркируйте источник: Header(), Cookie(), Form() — это и читаемость, и документация.
  • Для form-data ставьте python-multipart; не смешивайте формы с JSON-телом.
  • Чувствительные cookies помечайте HttpOnly, Secure, SameSite.
  • Стандартные заголовки авторизации лучше обрабатывать через security-зависимости, а не вручную через Header.

Загрузка файлов

Частный, но важный случай форм — загрузка файлов. Для этого есть тип UploadFile и маркер File(). UploadFile устроен умно: он не загружает весь файл в память целиком, а использует временный буфер с порогом, после которого данные уходят на диск. Это позволяет принимать большие файлы, не рискуя исчерпать оперативную память. Внутри обработчика у вас есть асинхронные методы read, write, seek, close, а также имя файла и тип содержимого. Как и обычные формы, загрузка файлов требует установленного python-multipart и формата multipart/form-data, поэтому в одном запросе нельзя смешать файл и JSON-тело. Если файлов несколько, объявляют list[UploadFile]. Это стандартный, безопасный по памяти способ принимать аватары, документы и вложения.

Итог: у каждого источника данных свой маркер. Заголовки сопоставляются регистронезависимо с заменой _ на -, формы — это URL-кодированное тело, и они несовместимы с JSON в одном запросе.

Проверьте себя
1. С каким HTTP-заголовком FastAPI сопоставит параметр user_agent: Annotated[str, Header()]?
AX-User-Agent
Buser_agent (точное совпадение по подчёркиванию)
Cuser-agent — подчёркивания заменяются на дефисы, сравнение регистронезависимо
DUserAgent
2. Можно ли в одном обработчике одновременно принять Form-поля и JSON-тело через Pydantic-модель?
AДа, без ограничений
BНет, запрос имеет один формат тела: либо form-data, либо JSON
CДа, но только в GET
DДа, если установить python-multipart