Опирается на правила:
R-HDR-1,R-HDR-2,R-HDR-3,R-HDR-4,R-HDR-X1из REST API Style Guide → раздел Заголовки и трассировка.
Важно знать
- Стандартные HTTP-заголовки объявляются через
Header(...)в сигнатуре функции — FastAPI извлекает их автоматически и включает в OpenAPI.- Кастомные заголовки — с доменным префиксом единым для всех сервисов проекта; префикс
X-запрещён по RFC 6648 (R-HDR-X1).Idempotency-Keyобязателен для POST-операций, безопасных при повторной отправке (деньги, создание заказов).traceparent(W3C Trace Context) прокидывается через весь граф сервисов;trace-idиз него попадает в тело ошибки RFC 9457.- FastAPI автоматически конвертирует snake_case-аргументы в HTTP-заголовки kebab-case (
idempotency_key→Idempotency-Key).- OpenTelemetry (
opentelemetry-instrumentation-fastapi) извлекаетtraceparentи прокидывает в исходящие запросы без ручного кода.Locationзаголовок при201 Createdвозвращается явно черезResponse— FastAPI не добавляет его автоматически.
Заголовки в FastAPI — часть публичного контракта так же, как URL и тела. Они объявляются в сигнатуре endpoint-функции и автоматически попадают в сгенерированную OpenAPI-спецификацию. Именно это отличает code-first от ручного описания: заголовок объявлен один раз — в коде, не в YAML.
Стандартные HTTP-заголовки
R-HDR-1: используются по назначению.
| Заголовок | Назначение | Пример значения |
|---|---|---|
Content-Type | тип тела запроса / ответа | application/json |
Accept | ожидаемый тип ответа | application/json |
Authorization | аутентификация | Bearer eyJhbGci... |
Location | URL созданного ресурса при 201 Created | /api/v1/orders/550e... |
ETag | версия ресурса для кеширования | "33a64df5" |
If-None-Match | условный GET | "33a64df5" |
If-Match | optimistic concurrency для PUT / PATCH | "33a64df5" |
Cache-Control | директивы кеширования ответа | private, max-age=60 |
FastAPI читает Accept и Authorization через стандартный Header(...). Content-Type FastAPI разбирает сам — объявлять его через параметр не нужно.
from fastapi import APIRouter, Header, Response, status
from typing import Annotated
router = APIRouter(prefix="/api/v1", tags=["Orders"])
@router.get(
"/orders/{order_id}",
operation_id="getOrder",
summary="Получить заказ по ID",
)
async def get_order(
order_id: str,
authorization: Annotated[str, Header()],
if_none_match: Annotated[str | None, Header()] = None,
) -> Response:
etag = '"33a64df5"'
if if_none_match == etag:
return Response(status_code=304)
order = await order_service.get(order_id)
return Response(
content=order.model_dump_json(exclude_none=True),
media_type="application/json",
headers={"ETag": etag, "Cache-Control": "private, max-age=60"},
)
Location при 201 Created
FastAPI не добавляет Location автоматически — нужно передать явно через параметр headers конструктора Response или через Response-зависимость:
from fastapi import APIRouter, Response, status
from fastapi.responses import JSONResponse
router = APIRouter(prefix="/api/v1", tags=["Orders"])
@router.post(
"/orders",
status_code=status.HTTP_201_CREATED,
operation_id="createOrder",
summary="Создать заказ",
)
async def create_order(
body: CreateOrderRequest,
response: Response,
) -> OrderResponse:
order = await order_service.create(body)
response.headers["Location"] = f"/api/v1/orders/{order.order_id}"
return order
Через response: Response FastAPI дополняет заголовки не заменяя тело — Pydantic-модель сериализуется штатно, а Location добавляется поверх.
Кастомные заголовки с доменным префиксом
R-HDR-2: доменный префикс выбирается один раз и применяется во всех сервисах проекта.
Sber-Request-Id: 550e8400-e29b-41d4-a716-446655440000
Sber-Client-Version: 3.2.1
Sber-Tenant-Id: corporate
В FastAPI параметр объявляется в snake_case — фреймворк автоматически маппит на kebab-case HTTP-заголовок:
from fastapi import APIRouter, Header
from typing import Annotated
from uuid import UUID
router = APIRouter(prefix="/api/v1", tags=["Products"])
@router.get(
"/products/{product_id}",
operation_id="getProduct",
summary="Получить продукт",
)
async def get_product(
product_id: UUID,
sber_request_id: Annotated[str | None, Header()] = None,
sber_client_version: Annotated[str | None, Header()] = None,
) -> ProductResponse:
return await product_service.get(product_id)
FastAPI транслирует sber_request_id → Sber-Request-Id, sber_client_version → Sber-Client-Version. Никакого ручного alias не нужно.
Sber-Request-Id ≠ traceparent:
Sber-Request-Id— идентификатор одного запроса от клиента (дедупликация, логирование на уровне сервиса).traceparent— идентификатор всей цепочки вызовов через несколько сервисов.
Idempotency-Key
R-HDR-3: безопасный retry для неидемпотентных POST-запросов.
POST /api/v1/orders
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{ "customerId": "cust-123", "items": [...] }
Контракт:
- Клиент генерирует ключ один раз на бизнес-операцию (не на HTTP-запрос).
- Повторный POST с тем же ключом → backend возвращает первый результат, заказ не создаётся дважды.
- Тот же ключ с другим payload →
409 Conflict.
В FastAPI — обязательный заголовок через Header(...):
from fastapi import APIRouter, Header, Response, status
from typing import Annotated
from uuid import UUID
router = APIRouter(prefix="/api/v1", tags=["Orders"])
@router.post(
"/orders",
status_code=status.HTTP_201_CREATED,
operation_id="createOrder",
summary="Создать заказ",
)
async def create_order(
body: CreateOrderRequest,
response: Response,
idempotency_key: Annotated[UUID, Header()],
) -> OrderResponse:
order = await order_service.create(body, idempotency_key=str(idempotency_key))
response.headers["Location"] = f"/api/v1/orders/{order.order_id}"
return order
FastAPI маппит idempotency_key → Idempotency-Key автоматически. Тип UUID обеспечивает валидацию формата на уровне Pydantic до вызова handler.
Если заголовок обязателен (нет дефолта) — FastAPI вернёт 422 при его отсутствии. После переопределения RequestValidationError-handler (см. python/errors) ответ будет 400 VALIDATION_ERROR с violations.
traceparent — W3C Trace Context
R-HDR-4: distributed tracing по стандарту W3C Trace Context.
traceparent: {version}-{trace-id}-{parent-id}-{trace-flags}
00-1f2a8b6c7d3e4f5a9b0c1d2e3f4a5b6c-7a8b9c0d1e2f3a4b-01
│ │ │ │
│ trace-id (32 hex) parent-id (16) flags
version
- version — формат, сейчас всегда
00. - trace-id — 32 hex, уникальный ID всей цепочки вызовов.
- parent-id — 16 hex, ID текущего span.
- trace-flags — 2 hex,
01= sampled.
Подключение OpenTelemetry
Предпочтительный путь — opentelemetry-instrumentation-fastapi: middleware перехватывает traceparent, создаёт span, прокидывает контекст в исходящие запросы через httpx / aiohttp.
# main.py
from fastapi import FastAPI
from opentelemetry import trace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
После подключения:
- Входящий
traceparent→ фреймворк использует егоtrace-id, создаёт новыйparent-idдля своего span. - Нет
traceparent→ генерируется новыйtrace-idна входе. trace-idдоступен в контексте черезtrace.get_current_span().get_span_context().trace_id.
trace-id в теле ошибки RFC 9457
trace-id из traceparent должен попасть в поле traceId тела ошибки — это позволяет клиенту передать его в поддержку:
from fastapi import Request
from fastapi.responses import Response
from fastapi.exceptions import RequestValidationError
from opentelemetry import trace
import json
def get_trace_id() -> str | None:
span = trace.get_current_span()
ctx = span.get_span_context()
if ctx.is_valid:
return format(ctx.trace_id, "032x")
return None
async def validation_exception_handler(
request: Request,
exc: RequestValidationError,
) -> Response:
violations = [
{"field": ".".join(str(loc) for loc in e["loc"][1:]), "message": e["msg"]}
for e in exc.errors()
]
body = {
"type": "urn:problem:orders:validation-error",
"title": "Validation Error",
"status": 400,
"code": "VALIDATION_ERROR",
"traceId": get_trace_id(),
"violations": violations,
}
return Response(
content=json.dumps({k: v for k, v in body.items() if v is not None}),
status_code=400,
media_type="application/problem+json",
)
Ручное чтение traceparent (когда нет OTel)
Если OpenTelemetry не подключён, traceparent читается как обычный заголовок:
from fastapi import APIRouter, Header
from typing import Annotated
router = APIRouter(prefix="/api/v1", tags=["Customers"])
@router.get(
"/customers/{customer_id}",
operation_id="getCustomer",
summary="Получить клиента",
)
async def get_customer(
customer_id: str,
traceparent: Annotated[str | None, Header()] = None,
) -> CustomerResponse:
trace_id = None
if traceparent:
parts = traceparent.split("-")
trace_id = parts[1] if len(parts) >= 2 else None
return await customer_service.get(customer_id, trace_id=trace_id)
Ручное чтение — только когда нет OTel. При наличии инструментации — trace-id доступен через API трейсера, дублировать параметр в сигнатуре не нужно.
tracestate
Опциональный заголовок для vendor-специфичных данных. В большинстве сервисов не нужен — OTel заполняет его если используется vendor-specific tracer.
traceparent: 00-1f2a8b6c...-7a8b9c0d...-01
tracestate: vendor1=value1,vendor2=value2
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
X-Request-Id: ... — префикс X- | R-HDR-X1 | Sber-Request-Id (доменный префикс) |
Кастомный заголовок без префикса (Request-Id) | R-HDR-2 | единый Shop-* / Sber-* для всех сервисов |
Authorization: eyJhbGci... без схемы Bearer | R-HDR-1 | Authorization: Bearer eyJhbGci... |
Idempotency-Key на GET / DELETE | R-HDR-3 | только для POST / PATCH |
Самописный Tracking-Id вместо traceparent | R-HDR-4 | W3C traceparent + OpenTelemetry |
trace-id 16 hex вместо 32 | R-HDR-4 | 32 hex строчными (format(id, "032x")) |
| Бизнес-данные в заголовке вместо тела | R-HDR-1 | тело запроса / path / query |
Location без /api/v1/ префикса в значении | R-HDR-1 | полный относительный путь /api/v1/orders/{id} |
Куда дальше
- python/errors.md —
traceIdв теле ошибки RFC 9457, переопределениеRequestValidationError-handler. - python/json-and-responses.md —
Locationдля201 Created,exclude_noneв ответах. - python/versioning.md —
DeprecationиSunsetзаголовки при выводе эндпоинта из эксплуатации. - python/rate-limiting-files-deprecation.md —
Retry-AfterиRateLimit-*при429. - Observability → tracing — OpenTelemetry,
traceparentpropagation черезhttpx. - REST API — нормативный индекс — формулировки
R-HDR-*.