Thanks to visit codestin.com
Credit goes to fastapi.tiangolo.com

Aller au contenu

ÉvĂ©nements envoyĂ©s par le serveur (SSE)

🌐 Traduction par IA et humains

Cette traduction a Ă©tĂ© rĂ©alisĂ©e par une IA guidĂ©e par des humains. đŸ€

Elle peut contenir des erreurs d'interprĂ©tation du sens original, ou paraĂźtre peu naturelle, etc. đŸ€–

Vous pouvez améliorer cette traduction en nous aidant à mieux guider le LLM d'IA.

Version anglaise

Vous pouvez diffuser des données vers le client en utilisant les Server-Sent Events (SSE).

C'est similaire à Diffuser des JSON Lines, mais cela utilise le format text/event-stream, pris en charge nativement par les navigateurs via l’API EventSource.

Remarque

Ajouté dans FastAPI 0.135.0.

Que sont les Server-Sent Events ?

SSE est un standard pour diffuser des données du serveur au client via HTTP.

Chaque événement est un petit bloc de texte avec des « champs » comme data, event, id et retry, séparés par des lignes vides.

Cela ressemble Ă  ceci :

data: {"name": "Portal Gun", "price": 999.99}

data: {"name": "Plumbus", "price": 32.99}

Les SSE sont couramment utilisĂ©s pour le streaming de chat IA, les notifications en direct, les journaux et l’observabilitĂ©, et d’autres cas oĂč le serveur envoie des mises Ă  jour au client.

Astuce

Si vous souhaitez diffuser des donnĂ©es binaires, par exemple de la vidĂ©o ou de l’audio, consultez le guide avancĂ© : Diffuser des donnĂ©es.

Diffuser des SSE avec FastAPI

Pour diffuser des SSE avec FastAPI, utilisez yield dans votre fonction de chemin d'accÚs et définissez response_class=EventSourceResponse.

Importez EventSourceResponse depuis fastapi.sse :

from collections.abc import AsyncIterable, Iterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None


items = [
    Item(name="Plumbus", description="A multi-purpose household device."),
    Item(name="Portal Gun", description="A portal opening device."),
    Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]


@app.get("/items/stream", response_class=EventSourceResponse)
async def sse_items() -> AsyncIterable[Item]:
    for item in items:
        yield item

# Code below omitted 👇
👀 Full file preview
from collections.abc import AsyncIterable, Iterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None


items = [
    Item(name="Plumbus", description="A multi-purpose household device."),
    Item(name="Portal Gun", description="A portal opening device."),
    Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]


@app.get("/items/stream", response_class=EventSourceResponse)
async def sse_items() -> AsyncIterable[Item]:
    for item in items:
        yield item


@app.get("/items/stream-no-async", response_class=EventSourceResponse)
def sse_items_no_async() -> Iterable[Item]:
    for item in items:
        yield item


@app.get("/items/stream-no-annotation", response_class=EventSourceResponse)
async def sse_items_no_annotation():
    for item in items:
        yield item


@app.get("/items/stream-no-async-no-annotation", response_class=EventSourceResponse)
def sse_items_no_async_no_annotation():
    for item in items:
        yield item

Chaque Ă©lĂ©ment produit avec yield est encodĂ© en JSON et envoyĂ© dans le champ data: d’un Ă©vĂ©nement SSE.

Si vous dĂ©clarez le type de retour comme AsyncIterable[Item], FastAPI l’utilisera pour valider, documenter et sĂ©rialiser les donnĂ©es avec Pydantic.

from collections.abc import AsyncIterable, Iterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None


items = [
    Item(name="Plumbus", description="A multi-purpose household device."),
    Item(name="Portal Gun", description="A portal opening device."),
    Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]


@app.get("/items/stream", response_class=EventSourceResponse)
async def sse_items() -> AsyncIterable[Item]:
    for item in items:
        yield item

# Code below omitted 👇
👀 Full file preview
from collections.abc import AsyncIterable, Iterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None


items = [
    Item(name="Plumbus", description="A multi-purpose household device."),
    Item(name="Portal Gun", description="A portal opening device."),
    Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]


@app.get("/items/stream", response_class=EventSourceResponse)
async def sse_items() -> AsyncIterable[Item]:
    for item in items:
        yield item


@app.get("/items/stream-no-async", response_class=EventSourceResponse)
def sse_items_no_async() -> Iterable[Item]:
    for item in items:
        yield item


@app.get("/items/stream-no-annotation", response_class=EventSourceResponse)
async def sse_items_no_annotation():
    for item in items:
        yield item


@app.get("/items/stream-no-async-no-annotation", response_class=EventSourceResponse)
def sse_items_no_async_no_annotation():
    for item in items:
        yield item

Astuce

Comme Pydantic le sérialisera du cÎté Rust, vous obtiendrez une performance bien supérieure que si vous ne déclarez pas de type de retour.

Fonctions de chemin d'accĂšs non async

Vous pouvez aussi utiliser des fonctions def normales (sans async), et utiliser yield de la mĂȘme façon.

FastAPI s’assure qu’elles s’exĂ©cutent correctement pour ne pas bloquer la boucle d’évĂ©nements.

Dans ce cas la fonction n’est pas async, le type de retour appropriĂ© serait Iterable[Item] :

# Code above omitted 👆

@app.get("/items/stream-no-async", response_class=EventSourceResponse)
def sse_items_no_async() -> Iterable[Item]:
    for item in items:
        yield item

# Code below omitted 👇
👀 Full file preview
from collections.abc import AsyncIterable, Iterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None


items = [
    Item(name="Plumbus", description="A multi-purpose household device."),
    Item(name="Portal Gun", description="A portal opening device."),
    Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]


@app.get("/items/stream", response_class=EventSourceResponse)
async def sse_items() -> AsyncIterable[Item]:
    for item in items:
        yield item


@app.get("/items/stream-no-async", response_class=EventSourceResponse)
def sse_items_no_async() -> Iterable[Item]:
    for item in items:
        yield item


@app.get("/items/stream-no-annotation", response_class=EventSourceResponse)
async def sse_items_no_annotation():
    for item in items:
        yield item


@app.get("/items/stream-no-async-no-annotation", response_class=EventSourceResponse)
def sse_items_no_async_no_annotation():
    for item in items:
        yield item

Sans type de retour

Vous pouvez aussi omettre le type de retour. FastAPI utilisera le jsonable_encoder pour convertir les données et les envoyer.

# Code above omitted 👆

@app.get("/items/stream-no-annotation", response_class=EventSourceResponse)
async def sse_items_no_annotation():
    for item in items:
        yield item

# Code below omitted 👇
👀 Full file preview
from collections.abc import AsyncIterable, Iterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None


items = [
    Item(name="Plumbus", description="A multi-purpose household device."),
    Item(name="Portal Gun", description="A portal opening device."),
    Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]


@app.get("/items/stream", response_class=EventSourceResponse)
async def sse_items() -> AsyncIterable[Item]:
    for item in items:
        yield item


@app.get("/items/stream-no-async", response_class=EventSourceResponse)
def sse_items_no_async() -> Iterable[Item]:
    for item in items:
        yield item


@app.get("/items/stream-no-annotation", response_class=EventSourceResponse)
async def sse_items_no_annotation():
    for item in items:
        yield item


@app.get("/items/stream-no-async-no-annotation", response_class=EventSourceResponse)
def sse_items_no_async_no_annotation():
    for item in items:
        yield item

ServerSentEvent

Si vous devez définir des champs SSE comme event, id, retry ou comment, vous pouvez produire des objets ServerSentEvent au lieu de données brutes.

Importez ServerSentEvent depuis fastapi.sse :

from collections.abc import AsyncIterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse, ServerSentEvent
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


items = [
    Item(name="Plumbus", price=32.99),
    Item(name="Portal Gun", price=999.99),
    Item(name="Meeseeks Box", price=49.99),
]


@app.get("/items/stream", response_class=EventSourceResponse)
async def stream_items() -> AsyncIterable[ServerSentEvent]:
    yield ServerSentEvent(comment="stream of item updates")
    for i, item in enumerate(items):
        yield ServerSentEvent(data=item, event="item_update", id=str(i + 1), retry=5000)

Le champ data est toujours encodé en JSON. Vous pouvez passer toute valeur sérialisable en JSON, y compris des modÚles Pydantic.

Données brutes

Si vous devez envoyer des données sans encodage JSON, utilisez raw_data au lieu de data.

C’est utile pour envoyer du texte prĂ©formatĂ©, des lignes de log, ou des valeurs « sentinelle » spĂ©ciales comme [DONE].

from collections.abc import AsyncIterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse, ServerSentEvent

app = FastAPI()


@app.get("/logs/stream", response_class=EventSourceResponse)
async def stream_logs() -> AsyncIterable[ServerSentEvent]:
    logs = [
        "2025-01-01 INFO  Application started",
        "2025-01-01 DEBUG Connected to database",
        "2025-01-01 WARN  High memory usage detected",
    ]
    for log_line in logs:
        yield ServerSentEvent(raw_data=log_line)

Remarque

data et raw_data s’excluent mutuellement. Vous ne pouvez en dĂ©finir qu’un seul par ServerSentEvent.

Reprendre avec Last-Event-ID

Quand un navigateur se reconnecte aprĂšs une coupure, il envoie le dernier id reçu dans l’en-tĂȘte Last-Event-ID.

Vous pouvez le lire comme paramĂštre d’en-tĂȘte et l’utiliser pour reprendre le flux lĂ  oĂč le client s’était arrĂȘtĂ© :

from collections.abc import AsyncIterable
from typing import Annotated

from fastapi import FastAPI, Header
from fastapi.sse import EventSourceResponse, ServerSentEvent
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    price: float


items = [
    Item(name="Plumbus", price=32.99),
    Item(name="Portal Gun", price=999.99),
    Item(name="Meeseeks Box", price=49.99),
]


@app.get("/items/stream", response_class=EventSourceResponse)
async def stream_items(
    last_event_id: Annotated[int | None, Header()] = None,
) -> AsyncIterable[ServerSentEvent]:
    start = last_event_id + 1 if last_event_id is not None else 0
    for i, item in enumerate(items):
        if i < start:
            continue
        yield ServerSentEvent(data=item, id=str(i))

SSE avec POST

SSE fonctionne avec n’importe quelle mĂ©thode HTTP, pas seulement GET.

C’est utile pour des protocoles comme MCP qui diffusent des SSE via POST :

from collections.abc import AsyncIterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse, ServerSentEvent
from pydantic import BaseModel

app = FastAPI()


class Prompt(BaseModel):
    text: str


@app.post("/chat/stream", response_class=EventSourceResponse)
async def stream_chat(prompt: Prompt) -> AsyncIterable[ServerSentEvent]:
    words = prompt.text.split()
    for word in words:
        yield ServerSentEvent(data=word, event="token")
    yield ServerSentEvent(raw_data="[DONE]", event="done")

Détails techniques

FastAPI met en Ɠuvre certaines bonnes pratiques SSE prĂȘtes Ă  l’emploi.

  • Envoyer un commentaire « keep alive » ping toutes les 15 secondes quand aucun message n’a Ă©tĂ© Ă©mis, pour Ă©viter que certains proxys ne ferment la connexion, comme suggĂ©rĂ© dans la SpĂ©cification HTML : Server-Sent Events.
  • DĂ©finir l’en-tĂȘte Cache-Control: no-cache pour empĂȘcher la mise en cache du flux.
  • DĂ©finir un en-tĂȘte spĂ©cial X-Accel-Buffering: no pour empĂȘcher le buffering dans certains proxys comme Nginx.

Vous n’avez rien Ă  faire, cela fonctionne prĂȘt Ă  l’emploi. đŸ€“