4 changed files with 379 additions and 0 deletions
@ -0,0 +1,63 @@ |
|||||
|
# JSON с байтами в Base64 { #json-with-bytes-as-base64 } |
||||
|
|
||||
|
Если вашему приложению нужно принимать и отправлять JSON-данные, но при этом необходимо включать в них бинарные данные, вы можете кодировать их в base64. |
||||
|
|
||||
|
## Base64 и файлы { #base64-vs-files } |
||||
|
|
||||
|
Сначала рассмотрите возможность использовать [Файлы в запросе](../tutorial/request-files.md) для загрузки бинарных данных и [Пользовательский HTTP-ответ — FileResponse](./custom-response.md#fileresponse--fileresponse-) для отправки бинарных данных вместо кодирования их в JSON. |
||||
|
|
||||
|
JSON может содержать только строки в кодировке UTF-8, поэтому он не может содержать «сырые» байты. |
||||
|
|
||||
|
Base64 может кодировать бинарные данные в строки, но для этого используется больше символов, чем в исходных бинарных данных, поэтому обычно это менее эффективно, чем обычные файлы. |
||||
|
|
||||
|
Используйте base64 только если вам действительно нужно включать бинарные данные в JSON и вы не можете использовать файлы для этого. |
||||
|
|
||||
|
## Pydantic `bytes` { #pydantic-bytes } |
||||
|
|
||||
|
Вы можете объявить Pydantic-модель с полями `bytes`, а затем использовать `val_json_bytes` в конфиге модели, чтобы указать использовать base64 для валидации входящих JSON-данных; как часть этой валидации строка base64 будет декодирована в байты. |
||||
|
|
||||
|
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:9,29:35] hl[9] *} |
||||
|
|
||||
|
Если вы откроете `/docs`, вы увидите, что поле `data` ожидает байты, закодированные в base64: |
||||
|
|
||||
|
<div class="screenshot"> |
||||
|
<img src="/img/tutorial/json-base64-bytes/image01.png"> |
||||
|
</div> |
||||
|
|
||||
|
Вы можете отправить такой HTTP-запрос: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"description": "Some data", |
||||
|
"data": "aGVsbG8=" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
/// tip | Совет |
||||
|
|
||||
|
`aGVsbG8=` — это base64-кодирование строки `hello`. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
Затем Pydantic декодирует строку base64 и передаст вам исходные байты в поле `data` модели. |
||||
|
|
||||
|
Вы получите такой HTTP-ответ: |
||||
|
|
||||
|
```json |
||||
|
{ |
||||
|
"description": "Some data", |
||||
|
"content": "hello" |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Pydantic `bytes` для выходных данных { #pydantic-bytes-for-output-data } |
||||
|
|
||||
|
Вы также можете использовать поля `bytes` с `ser_json_bytes` в конфиге модели для выходных данных, и Pydantic будет сериализовать байты в base64 при формировании JSON-ответа. |
||||
|
|
||||
|
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,12:16,29,38:41] hl[16] *} |
||||
|
|
||||
|
## Pydantic `bytes` для входных и выходных данных { #pydantic-bytes-for-input-and-output-data } |
||||
|
|
||||
|
И, конечно, вы можете использовать одну и ту же модель, настроенную на использование base64, чтобы обрабатывать и входящие данные (валидация) с `val_json_bytes`, и исходящие данные (сериализация) с `ser_json_bytes` при приеме и отправке JSON-данных. |
||||
|
|
||||
|
{* ../../docs_src/json_base64_bytes/tutorial001_py310.py ln[1:2,19:26,29,44:46] hl[23:26] *} |
||||
@ -0,0 +1,117 @@ |
|||||
|
# Потоковая передача данных { #stream-data } |
||||
|
|
||||
|
Если вам нужно передавать потоковые данные, которые можно представить как JSON, воспользуйтесь [стримингом JSON Lines](../tutorial/stream-json-lines.md). |
||||
|
|
||||
|
Но если вы хотите передавать в потоке чистые бинарные данные или строки, ниже показано, как это сделать. |
||||
|
|
||||
|
/// info | Информация |
||||
|
|
||||
|
Добавлено в FastAPI 0.134.0. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Варианты использования { #use-cases } |
||||
|
|
||||
|
Это можно использовать, если вы хотите стримить чистые строки, например, напрямую из вывода сервиса **AI LLM**. |
||||
|
|
||||
|
Также вы можете стримить **большие бинарные файлы**, передавая каждый чанк данных по мере чтения, без необходимости загружать всё в память сразу. |
||||
|
|
||||
|
Аналогично можно стримить **видео** или **аудио** — данные могут даже генерироваться по мере обработки и отправки. |
||||
|
|
||||
|
## «`StreamingResponse` с `yield`» { #a-streamingresponse-with-yield } |
||||
|
|
||||
|
Если вы укажете `response_class=StreamingResponse` в своей *функции-обработчике пути*, вы можете использовать `yield`, чтобы по очереди отправлять каждый чанк данных. |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial001_py310.py ln[1:23] hl[20,23] *} |
||||
|
|
||||
|
FastAPI будет передавать каждый чанк данных в `StreamingResponse` как есть, не пытаясь конвертировать его в JSON или что-то подобное. |
||||
|
|
||||
|
### Не-async *функции-обработчики пути* { #non-async-path-operation-functions } |
||||
|
|
||||
|
Можно использовать и обычные функции `def` (без `async`) и точно так же применять `yield`. |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial001_py310.py ln[26:29] hl[27] *} |
||||
|
|
||||
|
### Без аннотации { #no-annotation } |
||||
|
|
||||
|
Для потоковой передачи бинарных данных вам не нужно указывать аннотацию типа возвращаемого значения. |
||||
|
|
||||
|
Поскольку FastAPI не будет пытаться конвертировать данные в JSON с помощью Pydantic или сериализовать их каким-либо образом, в данном случае аннотация типа нужна только для вашего редактора кода и инструментов, FastAPI её использовать не будет. |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial001_py310.py ln[32:35] hl[33] *} |
||||
|
|
||||
|
Это также означает, что с `StreamingResponse` у вас есть и свобода, и ответственность — производить и кодировать байты данных ровно в том виде, в котором они должны быть отправлены, независимо от аннотаций типов. 🤓 |
||||
|
|
||||
|
### Потоковая передача байтов { #stream-bytes } |
||||
|
|
||||
|
Один из основных сценариев — стримить `bytes` вместо строк, и, конечно, это можно сделать. |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial001_py310.py ln[44:47] hl[47] *} |
||||
|
|
||||
|
## Пользовательский `PNGStreamingResponse` { #a-custom-pngstreamingresponse } |
||||
|
|
||||
|
В примерах выше байты данных передавались потоком, но в ответе не было HTTP-заголовка `Content-Type`, поэтому клиент не знал, какой тип данных он получает. |
||||
|
|
||||
|
Вы можете создать пользовательский подкласс `StreamingResponse`, который устанавливает HTTP-заголовок `Content-Type` в тип данных, которые вы стримите. |
||||
|
|
||||
|
Например, вы можете создать `PNGStreamingResponse`, который устанавливает HTTP-заголовок `Content-Type` в `image/png` с помощью атрибута `media_type`: |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial002_py310.py ln[6,19:20] hl[20] *} |
||||
|
|
||||
|
Затем вы можете использовать этот новый класс в `response_class=PNGStreamingResponse` в своей *функции-обработчике пути*: |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial002_py310.py ln[23:27] hl[23] *} |
||||
|
|
||||
|
### Симулировать файл { #simulate-a-file } |
||||
|
|
||||
|
В этом примере мы симулируем файл с помощью `io.BytesIO` — это «файлоподобный» объект, который существует только в памяти, но позволяет использовать тот же интерфейс. |
||||
|
|
||||
|
Например, мы можем итерироваться по нему, чтобы потреблять его содержимое, как и по обычному файлу. |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial002_py310.py ln[1:27] hl[3,12:13,25] *} |
||||
|
|
||||
|
/// note | Технические детали |
||||
|
|
||||
|
Две другие переменные, `image_base64` и `binary_image`, — это изображение, закодированное в Base64, а затем преобразованное в байты, после чего переданное в `io.BytesIO`. |
||||
|
|
||||
|
Только для того, чтобы всё помещалось в одном файле для этого примера, и вы могли скопировать код и запустить его как есть. 🥚 |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
Используя блок `with`, мы гарантируем, что объект, ведущий себя как файл, будет закрыт после завершения работы функции‑генератора (функции с `yield`). То есть после того, как она закончит отправку ответа. |
||||
|
|
||||
|
В этом конкретном примере это не столь важно, потому что это «фейковый» файл в памяти (`io.BytesIO`), но для реального файла важно удостовериться, что файл закрыт после завершения работы с ним. |
||||
|
|
||||
|
### Файлы и async { #files-and-async } |
||||
|
|
||||
|
В большинстве случаев «файлоподобные» объекты по умолчанию не совместимы с async и await. |
||||
|
|
||||
|
Например, у них нет `await file.read()` или `async for chunk in file`. |
||||
|
|
||||
|
И во многих случаях чтение таких объектов будет блокирующей операцией (которая может заблокировать цикл событий), потому что данные читаются с диска или из сети. |
||||
|
|
||||
|
/// info | Информация |
||||
|
|
||||
|
Приведённый выше пример — исключение, потому что объект `io.BytesIO` уже находится в памяти, поэтому чтение ничего не блокирует. |
||||
|
|
||||
|
Но во многих случаях чтение файла или «файлоподобного» объекта будет блокировать. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
Чтобы не блокировать цикл событий, вы можете просто объявить *функцию-обработчик пути* обычной `def` вместо `async def`. Тогда FastAPI выполнит её в воркере из пула потоков (threadpool), чтобы не блокировать основной цикл. |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial002_py310.py ln[30:34] hl[31] *} |
||||
|
|
||||
|
/// tip | Совет |
||||
|
|
||||
|
Если вам нужно вызвать блокирующий код изнутри async-функции, или async-функцию изнутри блокирующей функции, вы можете использовать [Asyncer](https://asyncer.tiangolo.com), родственную библиотеку к FastAPI. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
### `yield from` { #yield-from } |
||||
|
|
||||
|
Когда вы итерируетесь по чему‑то, например, по «файлоподобному» объекту, и делаете `yield` для каждого элемента, вы можете также использовать `yield from`, чтобы отдавать каждый элемент напрямую и не писать цикл `for`. |
||||
|
|
||||
|
Это не специфично для FastAPI, это просто Python, но полезный приём. 😎 |
||||
|
|
||||
|
{* ../../docs_src/stream_data/tutorial002_py310.py ln[37:40] hl[40] *} |
||||
@ -0,0 +1,88 @@ |
|||||
|
# Строгая проверка HTTP-заголовка Content-Type { #strict-content-type-checking } |
||||
|
|
||||
|
По умолчанию **FastAPI** использует строгую проверку HTTP-заголовка `Content-Type` для JSON-тел запросов. Это означает, что JSON-запросы должны включать корректный заголовок `Content-Type` (например, `application/json`), чтобы тело запроса было обработано как JSON. |
||||
|
|
||||
|
## Риск CSRF { #csrf-risk } |
||||
|
|
||||
|
Такое поведение по умолчанию обеспечивает защиту от класса атак **Cross-Site Request Forgery (CSRF)** в очень специфическом сценарии. |
||||
|
|
||||
|
Эти атаки используют то, что браузеры позволяют скриптам отправлять запросы без выполнения CORS preflight, когда они: |
||||
|
|
||||
|
- не имеют заголовка `Content-Type` (например, при использовании `fetch()` с телом `Blob`) |
||||
|
- и не отправляют никаких учетных данных аутентификации. |
||||
|
|
||||
|
Этот тип атак в основном актуален, когда: |
||||
|
|
||||
|
- приложение запускается локально (например, на `localhost`) или во внутренней сети |
||||
|
- и в приложении нет аутентификации, оно предполагает, что любому запросу из той же сети можно доверять. |
||||
|
|
||||
|
## Пример атаки { #example-attack } |
||||
|
|
||||
|
Представьте, что вы сделали способ запускать локального ИИ-агента. |
||||
|
|
||||
|
Он предоставляет API по адресу |
||||
|
|
||||
|
``` |
||||
|
http://localhost:8000/v1/agents/multivac |
||||
|
``` |
||||
|
|
||||
|
Есть также фронтенд по адресу |
||||
|
|
||||
|
``` |
||||
|
http://localhost:8000 |
||||
|
``` |
||||
|
|
||||
|
/// tip | Совет |
||||
|
|
||||
|
Обратите внимание, что у обоих один и тот же хост. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
Через фронтенд вы можете заставлять ИИ-агента выполнять действия от вашего имени. |
||||
|
|
||||
|
Так как он работает локально и не в открытом интернете, вы решаете не настраивать аутентификацию, полагаясь на доступ к локальной сети. |
||||
|
|
||||
|
Затем один из ваших пользователей может установить это и запускать локально. |
||||
|
|
||||
|
Потом он может открыть вредоносный сайт, например что-то вроде |
||||
|
|
||||
|
``` |
||||
|
https://evilhackers.example.com |
||||
|
``` |
||||
|
|
||||
|
И этот вредоносный сайт отправит запросы с помощью `fetch()` с телом `Blob` к локальному API по адресу |
||||
|
|
||||
|
``` |
||||
|
http://localhost:8000/v1/agents/multivac |
||||
|
``` |
||||
|
|
||||
|
Несмотря на то, что хост вредоносного сайта и локального приложения различается, браузер не запустит CORS preflight-запрос, потому что: |
||||
|
|
||||
|
- Приложение работает без аутентификации, ему не нужно отправлять какие-либо учетные данные. |
||||
|
- Браузер «считает», что он не отправляет JSON (из-за отсутствия заголовка `Content-Type`). |
||||
|
|
||||
|
В итоге вредоносный сайт может заставить локального ИИ-агента отправить злые сообщения бывшему начальнику пользователя... или что-то похуже. 😅 |
||||
|
|
||||
|
## Открытый интернет { #open-internet } |
||||
|
|
||||
|
Если ваше приложение доступно в открытом интернете, вы не будете «доверять сети» и позволять кому угодно отправлять привилегированные запросы без аутентификации. |
||||
|
|
||||
|
Злоумышленники могут просто запустить скрипт и слать запросы к вашему API, без участия браузера, так что вы, вероятно, уже защищаете любые привилегированные эндпоинты. |
||||
|
|
||||
|
В этом случае эта атака/риск на вас не распространяется. |
||||
|
|
||||
|
Этот риск и атака в основном актуальны, когда приложение работает в локальной сети и это единственная предполагаемая защита. |
||||
|
|
||||
|
## Разрешение запросов без Content-Type { #allowing-requests-without-content-type } |
||||
|
|
||||
|
Если вам нужно поддерживать клиентов, которые не отправляют заголовок `Content-Type`, вы можете отключить строгую проверку, установив `strict_content_type=False`: |
||||
|
|
||||
|
{* ../../docs_src/strict_content_type/tutorial001_py310.py hl[4] *} |
||||
|
|
||||
|
С этой настройкой запросы без заголовка `Content-Type` будут иметь тело запроса, обработанное как JSON — это такое же поведение, как в более старых версиях FastAPI. |
||||
|
|
||||
|
/// info | Информация |
||||
|
|
||||
|
Это поведение и настройка были добавлены в FastAPI 0.132.0. |
||||
|
|
||||
|
/// |
||||
@ -0,0 +1,111 @@ |
|||||
|
# Стриминг JSON Lines { #stream-json-lines } |
||||
|
|
||||
|
У вас может быть последовательность данных, которую вы хотите отправлять в «**потоке**». Это можно сделать с помощью **JSON Lines**. |
||||
|
|
||||
|
/// info | Информация |
||||
|
|
||||
|
Добавлено в FastAPI 0.134.0. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Что такое поток? { #what-is-a-stream } |
||||
|
|
||||
|
«**Стриминг**» данных означает, что ваше приложение начнет отправлять элементы данных клиенту, не дожидаясь готовности всей последовательности. |
||||
|
|
||||
|
То есть оно отправит первый элемент, клиент его получит и начнет обрабатывать, а вы в это время можете все еще генерировать следующий элемент. |
||||
|
|
||||
|
```mermaid |
||||
|
sequenceDiagram |
||||
|
participant App |
||||
|
participant Client |
||||
|
|
||||
|
App->>App: Produce Item 1 |
||||
|
App->>Client: Send Item 1 |
||||
|
App->>App: Produce Item 2 |
||||
|
Client->>Client: Process Item 1 |
||||
|
App->>Client: Send Item 2 |
||||
|
App->>App: Produce Item 3 |
||||
|
Client->>Client: Process Item 2 |
||||
|
App->>Client: Send Item 3 |
||||
|
Client->>Client: Process Item 3 |
||||
|
Note over App: Keeps producing... |
||||
|
Note over Client: Keeps consuming... |
||||
|
``` |
||||
|
|
||||
|
Это может быть даже бесконечный поток, когда вы продолжаете отправлять данные. |
||||
|
|
||||
|
## JSON Lines { #json-lines } |
||||
|
|
||||
|
В таких случаях часто отправляют «**JSON Lines**», это формат, в котором отправляется по одному JSON-объекту на строку. |
||||
|
|
||||
|
Ответ будет иметь тип содержимого `application/jsonl` (вместо `application/json`), а тело ответа будет примерно таким: |
||||
|
|
||||
|
```json |
||||
|
{"name": "Plumbus", "description": "A multi-purpose household device."} |
||||
|
{"name": "Portal Gun", "description": "A portal opening device."} |
||||
|
{"name": "Meeseeks Box", "description": "A box that summons a Meeseeks."} |
||||
|
``` |
||||
|
|
||||
|
Это очень похоже на JSON-массив (эквивалент списка Python), но вместо того чтобы быть обернутым в `[]` и иметь `,` между элементами, здесь **один JSON-объект на строку**, они разделены символом новой строки. |
||||
|
|
||||
|
/// info | Информация |
||||
|
|
||||
|
Важный момент в том, что ваше приложение сможет по очереди производить каждую строку, пока клиент потребляет предыдущие строки. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
/// note | Технические детали |
||||
|
|
||||
|
Так как каждый JSON-объект будет разделен новой строкой, в их содержимом не могут быть буквальные символы новой строки, но могут быть экранированные переводы строк (`\n`), что входит в стандарт JSON. |
||||
|
|
||||
|
Однако обычно об этом не нужно беспокоиться — всё делается автоматически, читайте дальше. 🤓 |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Варианты использования { #use-cases } |
||||
|
|
||||
|
Вы можете использовать это для стриминга данных из сервиса **AI LLM**, из **логов** или **телеметрии**, или из других типов данных, которые можно структурировать в элементы **JSON**. |
||||
|
|
||||
|
/// tip | Совет |
||||
|
|
||||
|
Если вы хотите стримить бинарные данные, например видео или аудио, посмотрите расширенное руководство: [Потоковая передача данных](../advanced/stream-data.md). |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
## Стриминг JSON Lines с FastAPI { #stream-json-lines-with-fastapi } |
||||
|
|
||||
|
Чтобы стримить JSON Lines с FastAPI, вместо использования `return` в вашей *функции-обработчике пути* используйте `yield`, чтобы по очереди выдавать каждый элемент. |
||||
|
|
||||
|
{* ../../docs_src/stream_json_lines/tutorial001_py310.py ln[1:24] hl[24] *} |
||||
|
|
||||
|
Если каждый JSON-элемент, который вы хотите отправить обратно, имеет тип `Item` (Pydantic-модель), и это асинхронная функция, вы можете объявить тип возвращаемого значения как `AsyncIterable[Item]`: |
||||
|
|
||||
|
{* ../../docs_src/stream_json_lines/tutorial001_py310.py ln[1:24] hl[9:11,22] *} |
||||
|
|
||||
|
Если вы объявите тип возвращаемого значения, FastAPI будет использовать его, чтобы **валидировать** данные, **документировать** их в OpenAPI, **фильтровать** и **сериализовать** с помощью Pydantic. |
||||
|
|
||||
|
/// tip | Совет |
||||
|
|
||||
|
Так как Pydantic будет сериализовывать это на стороне **Rust**, вы получите значительно более высокую **производительность**, чем если бы вы не указывали тип возвращаемого значения. |
||||
|
|
||||
|
/// |
||||
|
|
||||
|
### Неасинхронные функции-обработчики пути { #non-async-path-operation-functions } |
||||
|
|
||||
|
Вы также можете использовать обычные функции `def` (без `async`) и использовать `yield` таким же образом. |
||||
|
|
||||
|
FastAPI обеспечит корректное выполнение так, чтобы это не блокировало цикл событий. |
||||
|
|
||||
|
Поскольку в этом случае функция не асинхронная, подходящим типом возвращаемого значения будет `Iterable[Item]`: |
||||
|
|
||||
|
{* ../../docs_src/stream_json_lines/tutorial001_py310.py ln[27:30] hl[28] *} |
||||
|
|
||||
|
### Без возвращаемого типа { #no-return-type } |
||||
|
|
||||
|
Вы также можете опустить тип возвращаемого значения. Тогда FastAPI использует [`jsonable_encoder`](./encoder.md), чтобы преобразовать данные к виду, который можно сериализовать в JSON, и затем отправит их как JSON Lines. |
||||
|
|
||||
|
{* ../../docs_src/stream_json_lines/tutorial001_py310.py ln[33:36] hl[34] *} |
||||
|
|
||||
|
## События, отправляемые сервером (SSE) { #server-sent-events-sse } |
||||
|
|
||||
|
FastAPI также имеет полноценную поддержку Server-Sent Events (SSE), которые довольно похожи, но с парой дополнительных деталей. Вы можете узнать о них в следующей главе: [События, отправляемые сервером (SSE)](server-sent-events.md). 🤓 |
||||
Loading…
Reference in new issue