Ă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.
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 »
pingtoutes 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-cachepour empĂȘcher la mise en cache du flux. - DĂ©finir un en-tĂȘte spĂ©cial
X-Accel-Buffering: nopour empĂȘcher le buffering dans certains proxys comme Nginx.
Vous nâavez rien Ă faire, cela fonctionne prĂȘt Ă lâemploi. đ€