Retour au cours

backend / graphql

Subscriptions : temps réel

Leçon 111 exercice

Explication

Ce que vous allez apprendre

  • Comprendre en quoi une subscription diffère fondamentalement d'une query ou d'une mutation
  • Déclarer un champ Subscription et l'implémenter comme un générateur asynchrone
  • Comprendre pourquoi le transport passe par WebSocket plutôt que par un simple POST HTTP
  • Relier une mutation qui publie un événement à une subscription qui l'écoute via un pub/sub
  • Filtrer côté serveur ce que chaque client abonné a réellement le droit de recevoir

Dans quel contexte ?

Une application de suivi de livraison affiche en direct le statut d'une commande ("en préparation", "expédiée", "livrée") sans que l'utilisateur ait besoin de rafraîchir la page ou de reposer une requête toutes les cinq secondes. Un opérateur logistique change le statut de la commande depuis un back-office ; tous les clients abonnés à cette commande précise doivent recevoir la mise à jour immédiatement.

D'abord, une requête ponctuelle ne suffit plus

Une query classique répond une fois et se termine. Pour du temps réel, il faudrait la rejouer en boucle (polling), ce qui gaspille des ressources et introduit un délai. GraphQL propose un troisième type racine, Subscription, aux côtés de Query et Mutation, pensé spécifiquement pour représenter un flux continu d'événements plutôt qu'une réponse unique.

Une fois cette idée posée, il faut comprendre le changement de transport

Contrairement aux queries et mutations qui passent par un simple POST HTTP classique, une subscription nécessite une connexion persistante : le protocole standard est graphql-ws, transporté sur WebSocket. Le client "ouvre la ligne" une fois, et le serveur y pousse des messages au fil du temps, sans que le client ait besoin de renvoyer une requête à chaque fois.

Ensuite, côté serveur, une subscription s'écrit différemment d'un resolver classique

Plutôt que de retourner une valeur unique, une fonction de subscription est un générateur asynchrone (async def ... -> AsyncGenerator en Python avec yield, un objet avec subscribe en JavaScript) : elle produit une nouvelle valeur à chaque fois qu'un événement pertinent survient, potentiellement pendant des heures pour une connexion ouverte.

Type racineRéponseTransport
QueryUne réponse uniquePOST HTTP
MutationUne réponse unique, exécution séquentiellePOST HTTP
SubscriptionUn flux de réponses dans le tempsWebSocket (graphql-ws)

Il reste une question centrale : comment un événement arrive-t-il jusqu'au générateur ?

Un système pub/sub (souvent Redis en production multi-instance, pour que l'événement traverse plusieurs serveurs applicatifs) fait le lien : une mutation classique, comme changerStatutCommande, publie un événement sur un canal après avoir modifié la base de données ; le générateur de la subscription, abonné à ce même canal, reçoit l'événement et le transforme en une nouvelle valeur GraphQL renvoyée au client.

Prérequis

Cette leçon suppose que tu es à l'aise avec les mutations (leçon 4) et le concept de fonction asynchrone : une subscription combine les deux dans un mécanisme de flux continu.

Piège fréquent

Oublier de filtrer, côté serveur, ce qu'un abonné a le droit de recevoir est une faille de sécurité fréquente sur les subscriptions : un client qui s'abonne à nouveauMessage(conversationId: "X") ne doit recevoir les messages de cette conversation QUE s'il en fait réellement partie. L'autorisation doit être vérifiée dans la fonction de subscription elle-même, pas seulement au moment de la requête initiale de connexion.

Maintenant que tu sais faire circuler des données en temps réel, la prochaine leçon redescend au niveau très concret : comment brancher un vrai serveur GraphQL (Strawberry) sur une application FastAPI existante, avec authentification et base de données.

Commandes & code

Subscriptions : temps réel

graphql
# Le troisième type racine, après Query et Mutation : un flux continu d'événements
type Subscription {
  commandeStatutChange(commandeId: ID!): Commande!
  nouveauMessage(conversationId: ID!): Message!
}

type Commande {
  id: ID!
  statut: StatutCommande!
}
graphql
# Le client ouvre une connexion persistante (WebSocket) et reçoit des mises à jour
subscription SuivreCommande($id: ID!) {
  commandeStatutChange(commandeId: $id) {
    id
    statut
  }
}
python
# Strawberry : une subscription est un générateur asynchrone (async generator)
import strawberry
import asyncio
from typing import AsyncGenerator

@strawberry.type
class Subscription:
    @strawberry.subscription
    async def commande_statut_change(self, commande_id: strawberry.ID) -> AsyncGenerator[Commande, None]:
        # S'abonne à un pub/sub (Redis, in-memory) filtré sur cette commande précise
        async for evenement in pubsub.subscribe(f"commande.{commande_id}"):
            yield Commande(id=evenement["id"], statut=evenement["statut"])

schema = strawberry.Schema(query=Query, mutation=Mutation, subscription=Subscription)
python
# Publier un événement depuis une mutation classique (déclenche la subscription abonnée)
import strawberry

@strawberry.type
class Mutation:
    @strawberry.mutation
    async def changer_statut_commande(self, commande_id: strawberry.ID, statut: StatutCommande) -> Commande:
        commande = await db.mettre_a_jour_statut(commande_id, statut)
        # Tous les clients abonnés à cette commande reçoivent immédiatement l'événement
        await pubsub.publish(f"commande.{commande_id}", {"id": commande_id, "statut": statut})
        return commande
javascript
// Apollo Server + graphql-ws : transport WebSocket pour les subscriptions
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');

const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });

useServer(
  {
    schema,
    context: async (ctx) => ({
      pubsub,
      utilisateur: await authentifierWebSocket(ctx.connectionParams),
    }),
  },
  wsServer
);

const resolvers = {
  Subscription: {
    nouveauMessage: {
      subscribe: (_parent, { conversationId }, { pubsub }) =>
        pubsub.asyncIterator(`MESSAGE_${conversationId}`),
    },
  },
};

Résumé

  • Une subscription est un flux (async generator côté serveur), pas une requête ponctuelle.
  • Le transport est WebSocket (protocole graphql-ws), distinct du POST HTTP utilisé pour queries/mutations.
  • Un pub/sub (souvent Redis en production multi-instance) relie les mutations qui publient aux subscriptions abonnées.
  • Toujours filtrer côté serveur ce que chaque abonné a le droit de recevoir (autorisation par canal).

Exercices pratiques

1 disponible
1

Mission : sécuriser un fil de messagerie en temps réel

Objectif : Comprendre le transport WebSocket des subscriptions et implémenter un filtre d'autorisation exécuté à chaque événement, pas seulement à la connexion.

Contexte

Une messagerie interne propose nouveauMessage(conversationId: ID!) en subscription. Un audit de sécurité relève qu'un utilisateur authentifié pourrait s'abonner à l'id d'une conversation à laquelle il n'appartient pas, simplement en devinant ou en énumérant des identifiants, et recevoir ainsi des messages qui ne le concernent pas.

Résoudre l’exercice →