CodeGym /Kurse /ChatGPT Apps /Streaming-Kanäle: SSE und HTTP/stream – wann und wie man ...

Streaming-Kanäle: SSE und HTTP/stream – wann und wie man sie einsetzt

ChatGPT Apps
Level 13 , Lektion 1
Verfügbar

1. Wo tauchen „Streams“ in der Architektur der ChatGPT App überhaupt auf

Bevor wir darüber streiten, was besser ist – SSE oder HTTP-stream –, ist es sinnvoll zu verstehen, wo in unserem Stack überhaupt Streams existieren.

Grob lassen sich drei Ebenen unterscheiden.

Erstens: die Ebene von ChatGPT und dem Modell. Das Modell streamt die Antwort bereits tokenweise: Sie sehen, wie der Antworttext Buchstabe für Buchstabe „getippt“ wird. Das ist ebenfalls ein Stream, wird aber komplett von OpenAI gesteuert und betrifft Ihren Code nicht direkt.

Zweitens: die MCP‑Ebene. Wenn sich ChatGPT mit Ihrem MCP‑Server verbindet, hält es in der Regel eine SSE‑Verbindung: Der Server pusht darüber MCP‑JSON‑RPC‑Nachrichten (Antworten und Benachrichtigungen), und ChatGPT sendet im Gegenzug Anfragen an einen separaten HTTP‑Endpoint, zum Beispiel /messages. In MCP‑Begriffen ist das der Basistransport.

Drittens: die Ebene des Apps SDK und Ihres Backends. Ihr React‑Widget GiftGenius läuft in der ChatGPT‑Sandbox und kommuniziert mit Ihrem Backend/MCP‑Gateway über HTTP: entweder per normalem fetch, per fetch mit Stream (ReadableStream) oder über ein SSE‑Abonnement (EventSource).

Wichtig ist, diese Ebenen nicht durcheinanderzubringen. MCP‑Events sind „die Leitung“ zwischen ChatGPT und Ihren Servern; SSE/HTTP-stream zwischen Widget und Ihrem HTTP‑Backend – das ist Ihr eigener Streckenabschnitt.

Man kann das als Diagramm darstellen.

flowchart TD
  subgraph ChatGPT
    UI[ChatGPT UI + Modell]
    W[GiftGenius Widget]
  end

  subgraph YourInfra[Entwicklerinfrastruktur]
    GW[MCP Gateway / Backend]
    MCP[MCP Server]
  end

  UI -- "tool-call / Antworten\n(internes Token-Streaming)" --> W

  UI <-- "MCP über SSE\n(/sse + /messages)" --> MCP

  W <-- "HTTP / fetch / SSE / stream" --> GW

  GW <-- "JSON-RPC MCP" --> MCP

Heute konzentrieren wir uns auf den Pfeil Widget ↔ Backend und erinnern uns nebenbei daran, dass der MCP‑Transport selbst ebenfalls auf SSE basiert.

Genau auf diesem Abschnitt – Widget ↔ Backend – müssen wir entscheiden, wie wir kommunizieren: mit einfachen HTTP‑Anfragen oder mit Streams. Im nächsten Abschnitt sehen wir, warum „normales“ HTTP hier schnell nicht mehr reicht.

2. Warum eine gewöhnliche HTTP‑Anfrage nicht ausreicht

Das Standardmodell von HTTP ist „Anfrage → eine Antwort“. Der Client fragt etwas an, der Server antwortet einmal, die Verbindung wird geschlossen.

Für viele Aufgaben genügt das: den aktuellen Status eines job abrufen, Benutzereinstellungen speichern, eine fertige Geschenkeliste aus der Datenbank holen.

Sobald Sie jedoch eine lang laufende Operation bauen, beginnt es zu knirschen.

Stellen Sie sich GiftGenius vor, das:

  • Signale aus mehreren Quellen sammelt (Kaufhistorie, Wunschliste, soziale Netzwerke),
  • das Ganze durch ein paar LLM‑Anfragen jagt,
  • ein persönliches Ranking aus Hunderten Kandidaten erstellt.

Das kann Dutzende Sekunden dauern. Wenn Sie eine normale HTTP‑Anfrage 40 Sekunden offen halten und schweigen, wirkt der UX wie in alten Browsern: Der Nutzer starrt auf einen Spinner und fragt sich, ob die App abgestürzt ist oder noch „nachdenkt“.

Neben dem UX gibt es auch rein technische Probleme:

  • Timeouts bei ChatGPT, bei Vercel, bei Proxys;
  • keine Möglichkeit, Fortschritt, Partial Results etc. zu senden;
  • keine saubere Behandlung von Verbindungsabbrüchen und Wiederaufnahme.

Daraus ergibt sich die naheliegende Lösung: vom einen großen Antwortpaket zu einem Stream aus kleinen Häppchen wechseln, die der Server nach und nach senden kann.

Diese Häppchen können sein:

  • Ereignisse (job.progress, job.completed) – das ist SSE;
  • Fragmente einer großen Nutzlast (Berichtstext, NDJSON‑Zeilen mit Geschenken) – das ist HTTP-stream.

3. SSE (Server‑Sent Events): Ereignisse abonnieren

Beginnen wir mit SSE, denn es ist MCP in vieler Hinsicht „verwandt“: MCP nutzt selbst über HTTP eine SSE‑Verbindung, um Events vom Server zum Client zu pushen.

SSE‑Modell in einfachen Worten

SSE ist ein Protokoll oberhalb von normalem HTTP:

  1. Der Client öffnet eine GET‑Anfrage auf einen Endpoint, der mit Content-Type: text/event-stream antwortet;
  2. der Server schließt die Verbindung nicht, sondern schreibt periodisch Zeilen hinein wie:
event: job.progress
data: {"jobId":"123","percent":40}

event: job.completed
data: {"jobId":"123","resultCount":12}
  1. Auf Browserseite nutzt man EventSource, das:
    • automatisch Reconnects handhabt;
    • das Format event: + data: + doppelte Leerzeile parst;
    • die Handler onmessage / addEventListener("job.progress", ...) aufruft.

Der entscheidende Punkt: Der Kanal ist einseitig. Nur der Server sendet Events an den Client. Der Client schickt über diese Verbindung keine Daten zurück.

Für ChatGPT Apps passt dieses Modell hervorragend, wenn das Widget sich einfach per jobId „abonnieren“ soll und auf Fortschritt und Abschluss reagieren möchte.

Mini‑Beispiel eines SSE‑Endpoints in Next.js 16

Nehmen wir einen Route‑Handler für Job‑Fortschrittsereignisse an:

app/api/gift-jobs/[jobId]/events/route.ts

import { NextRequest } from "next/server";

export async function GET(req: NextRequest, { params }: { params: { jobId: string } }) {
  const jobId = params.jobId;

  const stream = new ReadableStream({
    start(controller) {
      // Hilfsfunktion zum Senden eines SSE-Events
      const send = (event: string, data: unknown) => {
        const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
        controller.enqueue(new TextEncoder().encode(payload));
      };

      send("job.started", { jobId });

      let percent = 0;
      const interval = setInterval(() => {
        percent += 20;
        if (percent >= 100) {
          send("job.completed", { jobId, totalGifts: 10 });
          clearInterval(interval);
          controller.close();
        } else {
          send("job.progress", { jobId, percent });
        }
      }, 1000);
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream", // hier setzen wir SSE
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
    },
  });
}

Das ist eine kleine Simulation: Einmal pro Sekunde steigt der Prozentwert, und am Ende kommt job.completed. Später ersetzen Sie den Timer durch echte Events eines Workers/einer Warteschlange, aber das Schema bleibt gleich.

Client: SSE‑Abonnement im GiftGenius‑Widget

Innerhalb des React‑Widgets können wir uns bei vorhandener jobId auf diesen Stream abonnieren. Zur Erinnerung: Die Widget‑API läuft in der ChatGPT‑Sandbox, aber EventSource ist dort genauso verfügbar wie im normalen Browser.

import { useEffect, useState } from "react";

export function GiftJobProgress({ jobId }: { jobId: string }) {
  const [percent, setPercent] = useState(0);

  useEffect(() => {
    const url = `/api/gift-jobs/${jobId}/events`;
    const es = new EventSource(url);

    es.addEventListener("job.progress", (event) => {
      const data = JSON.parse((event as MessageEvent).data);
      setPercent(data.percent);
    });

    es.addEventListener("job.completed", () => {
      setPercent(100);
      es.close();
    });

    es.onerror = () => {
      // hier kann man "Verbindungsprobleme, versuchen erneut zu verbinden" anzeigen
    };

    return () => es.close();
  }, [jobId]);

  return <div>Fortschritt der Geschenkfindung: {percent}%</div>;
}

Jetzt können Sie das mit einem MCP‑Tool verknüpfen. Das Tool start_gift_job gibt jobId zurück, und im ToolOutput Ihres Widgets rendern Sie einfach GiftJobProgress.

Autoreconnect und Last‑Event‑ID

EventSource versucht gemäß Standard automatisch, die Verbindung wiederherzustellen, wenn sie abreißt. Der Server kann das Standardfeld id: in Events verwenden, und der Client den Header Last-Event-ID, um nach einem Reconnect verpasste Events nachzuholen.

Für ein einfaches GiftGenius müssen Sie weder id: noch einen separaten Event‑Identifier sofort implementieren und können einen kleinen „Fortschrittsverlust“ beim Reconnect tolerieren. In Produktion, insbesondere unter Last, brauchen Sie jedoch:

  • das Standardfeld id: in jedes SSE‑Event aufzunehmen, damit der Client bei der Wiederverbindung Last-Event-ID senden kann;
  • eine anwendungsseitige event_id im Payload und Idempotenzlogik auf Client/Backend‑Seite zu verwenden.

Das passt direkt zu Idempotenz: Selbst wenn dasselbe job.progress zweimal eintrifft, führt der Handler, der eine bekannte event_id sieht, keine Seiteneffekte ein zweites Mal aus.

Unterm Strich bietet SSE ein bequemes Abonnement von Events rund um die jobId mit Autoreconnect und Dubletten‑Kontrolle über Event‑IDs. Schauen wir uns nun den zweiten Typ von Streams an – wenn wir eine einzelne Anfrage haben, deren sehr große Antwort wir stückweise liefern möchten.

4. HTTP‑Streaming: schrittweise auf eine Anfrage antworten

Wenn SSE ein „Abonnement unabhängiger Events“ ist, dann ist HTTP‑Streaming „eine Anfrage, eine Antwort – aber zeitlich gestreckt und in Chunks“.

Das ist genau der Mechanismus, den Sie sehen, wenn Sie die OpenAI‑API mit stream : true verwenden: Der Server sendet JSON‑Chunks (oft im SSE‑Format, aber semantisch „eine Anfrage ↔ ein Strom von Teilantworten“), und der Client setzt sie zu einem Endtext zusammen.

In Ihren eigenen APIs können Sie dasselbe tun für:

  • große Textberichte (z. B. die Erklärung der Logik hinter den gewählten Geschenken),
  • lange Geschenkelisten (stückweise streamen, statt den Nutzer warten zu lassen).

Einfachster HTTP‑Stream‑Endpoint in Next.js

Angenommen, wir müssen eine „Erklärung“ des Ergebnisses generieren, in der ein LLM einen langen Text schreibt. Wir möchten ihn beim Schreiben ins Widget streamen.

app/api/gift-report/route.ts

import { NextRequest } from "next/server";

export async function POST(req: NextRequest) {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();

      controller.enqueue(encoder.encode("Analyse starten...\n"));

      // Hier könnte die echte LLM‑Generierung mit Chunks stehen
      for (const line of ["Präferenzen sammeln...\n", "Budget berechnen...\n", "Abschließende Empfehlungen...\n"]) {
        await new Promise((r) => setTimeout(r, 1000));
        controller.enqueue(encoder.encode(line));
      }

      controller.close();
    },
  });

  return new Response(stream, {
    headers: {
      "Content-Type": "text/plain; charset=utf-8",
      "Transfer-Encoding": "chunked", // Hier geben wir an, dass es HTTP/Stream ist
    },
  });
}

Technisch kümmert sich Next selbst um die chunked‑Kodierung; wichtig ist nur, dass Sie eine ReadableStream zurückgeben.

HTTP‑Stream im Widget via fetch lesen

Auf dem Client (im Widget) kann man den Stream so lesen:

async function fetchReport(setText: (s: string) => void) {
  const res = await fetch("/api/gift-report", { method: "POST" });
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();

  let acc = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    acc += decoder.decode(value, { stream: true });
    setText(acc); // UI schrittweise aktualisieren
  }
}

Und die Wrapper‑Komponente:

import { useState } from "react";

export function GiftReport() {
  const [text, setText] = useState("");

  return (
    <div>
      <button onClick={() => fetchReport(setText)}>Bericht generieren</button>
      <pre style={{ whiteSpace: "pre-wrap" }}>{text}</pre>
    </div>
  );
}

Das ist ein klassisches Muster: eine POST‑Anfrage an /api/gift-report, als Antwort ein Textstream, den Sie schrittweise anzeigen.

JSON statt Text streamen

Oft möchte man nicht Strings, sondern JSON‑Objekte streamen. Das populärste Format ist NDJSON (Newline‑delimited JSON): Jedes Event ist eine JSON‑Zeile und endet mit dem Zeichen \n.

Beispiel Serverseite:

const stream = new ReadableStream({
  async start(controller) {
    const encoder = new TextEncoder();
    for (let i = 0; i < 5; i++) {
      const chunk = { type: "gift", index: i, name: `Geschenk #${i}` };
      controller.enqueue(encoder.encode(JSON.stringify(chunk) + "\n"));
      await new Promise((r) => setTimeout(r, 500));
    }
    controller.close();
  },
});

Der Client liest mit dem TextDecoder, splittet an \n und parst die einzelnen JSON‑Objekte.

5. SSE vs HTTP‑stream: Unterschiede und Auswahl

An diesem Punkt sollte das Bild intuitiv sein, aber fassen wir es trotzdem in einer kleinen Tabelle zusammen.

Eigenschaft SSE (Server‑Sent Events) HTTP‑Stream (chunked)
Initiator Client macht GET und abonniert Client stellt Anfrage (GET/POST), Server streamt die Antwort
Richtung Nur Server → Client Antwort des Servers auf eine konkrete Anfrage
Semantik Abonnement eines Ereignisstroms (pub/sub) Teilantwort auf eine einzelne Anfrage
Eingebautes Protokoll Vorhanden (event:, data:, id: etc.) Nicht vorhanden, Format definieren Sie selbst (Zeilen, NDJSON, JSON)
Client‑API EventSource fetch + ReadableStream / response.body
Reconnect‑Unterstützung Eingebaut (EventSource, Last-Event-ID) Muss manuell implementiert werden
Typische Anwendungsfälle Fortschritt, Status, Benachrichtigungen nach jobId Streaming von Text, großen JSON‑Antworten, LLM‑Output

Vereinfacht als „Faustregeln“ (mit Augenmaß):

  • Sie haben einen job und viele Events darum herum → SSE;
  • ein Tool‑Aufruf liefert ein großes Ergebnis, das Sie schrittweise zeigen möchten → HTTP‑Stream.

Für GiftGenius heißt das: SSE – für einen Live‑Fortschrittsbalken und Status des Auswahlprozesses; HTTP‑Stream – für eine lange Textzusammenfassung oder für das schrittweise Laden einer langen Geschenkeliste.

6. Wie das mit MCP und GiftGenius zusammenpasst

Erinnern wir uns an unser Schema vom Beginn der Vorlesung: Modell ↔ MCP ↔ Widget ↔ Backend. Wir haben bereits die Streams auf der Ebene Widget ↔ Backend betrachtet, gehen jetzt einen Schritt zurück und trennen sauber, wo MCP im Spiel ist und wo „nur HTTP“.

MCP definiert, wie ChatGPT (als MCP‑Client) mit Ihrem MCP‑Server spricht. Dafür gibt es den Transport, bei dem:

  • ChatGPT eine SSE‑Verbindung zu /sse öffnet und darüber MCP‑Nachrichten erhält (Antworten, Benachrichtigungen, Events);
  • ChatGPT MCP‑Anfragen (call_tool, list_tools usw.) an /messages sendet, meist als POST mit JSON‑RPC.

Diese Ebene haben Sie bereits gemeistert, als Sie GiftGenius an ChatGPT angebunden haben.

Wenn wir nun asynchrone Aufgaben und UX‑Streams im Widget hinzufügen, entstehen zwei Architekturvarianten.

Variante eins – „reines MCP“: Der MCP‑Server erzeugt selbst job.progress‑ und job.completed‑Events; ChatGPT erhält sie über MCP‑SSE; anschließend ruft das Modell Ihr Widget mit aktualisiertem Kontext auf, und das Widget rendert den Fortschritt, ohne direkt mit dem Backend zu sprechen. Das ist der maximal „kanonische“ Weg der MCP‑Events.

Variante zwei – hybrid: Das MCP‑Tool start_gift_job erstellt eine Aufgabe und gibt eine jobId zurück; das Widget erhält die jobId und kommuniziert dann selbst per HTTP mit dem Backend, abonniert den SSE‑Endpoint /api/gift-jobs/{jobId}/events und fordert bei Bedarf den HTTP‑Stream des Berichts an. Auf MCP‑Seite passiert dabei nichts Besonderes.

In diesem Kurs gehen wir den hybriden Weg: Er fügt sich besser in App Router/Next ein und ist einfacher lokal zu debuggen. Später können Sie nach und nach auf „reine MCP‑Benachrichtigungen“ umstellen, wenn Sie ein Gefühl dafür bekommen haben.

7. Reconnect, Timeouts und andere Realitäten des Netzwerks

Bisher klang alles ideal: SSE oder Stream öffnen, alles fließt, Events kommen, der UX glänzt. In der Realität reißen Verbindungen gerne in ungünstigen Momenten ab, und die Infrastruktur setzt Timeouts.

Was schiefgehen kann

Mit SSE und HTTP-stream werden Sie früher oder später konfrontiert mit:

  • Idle‑Timeouts auf dem Proxy: „Wenn über die Verbindung N Sekunden nichts kam – schließen“;
  • Neustarts Ihres Backends (Deploy, Störung);
  • instabilen Netzen auf Nutzerseite (insbesondere mobil).

Das ist normal; wichtig ist, vorbereitet zu sein, statt auf „wird schon gutgehen“ zu hoffen.

Strategie für SSE

SSE hat in diesem Bereich viele Pluspunkte:

  • EventSource verbindet sich nach einer kurzen Verzögerung selbst wieder;
  • Sie haben id: und Last-Event-ID, um Events nachzuholen.

Minimaler Praxis‑Werkzeugkasten:

  1. Auf dem Server regelmäßig etwas wie einen Heartbeat senden, damit die Verbindung nicht als vollständig idle gilt. Das kann ein separates Event event: ping oder einfach ein Kommentar : keep-alive sein.
  2. Auf dem Client in onerror einen verständlichen Status anzeigen wie „Verbindungsprobleme, versuchen erneut zu verbinden…“, statt das gesamte Widget zu zerlegen.
  3. Beim Reconnect – wenn Sie id: verwenden – vom Server nur neue Events nach dieser ID liefern. Für GiftGenius können Sie zunächst ganz ohne id: starten und den Zustand einfach nach dem zuletzt empfangenen job.progress/job.completed „rekonstruieren“.

Strategie für HTTP‑Stream

Der HTTP‑Stream ist eine einzelne Anfrage, daher müssen Sie bei einem Abbruch im Grunde neu anfangen:

  • Wenn Sie einen Textbericht streamen, können Sie dem Nutzer einfach sagen: „Der vollständige Bericht konnte nicht geladen werden, bitte erneut versuchen“ – und von vorne beginnen;
  • wenn Sie strukturierte Daten (NDJSON) streamen, können Sie über einen Resume‑Mechanismus nachdenken: etwa indem Sie in der Anfrage einen offset oder cursor übergeben, ab dem fortgesetzt werden soll.

Für den Anfang können Sie es einfach halten: Wenn der Antwortstream vorzeitig abbricht – zeigen Sie, was bereits da ist, und bieten Sie eine Schaltfläche „Berichtserstellung fortsetzen“, die eine neue Anfrage schickt.

Wichtig ist, den Nutzer nicht im Zustand „ewigen Wartens“ zu lassen.

8. Anwendung auf GiftGenius: End‑to‑End‑Szenario

Jetzt kombinieren wir alles, was wir zu SSE, HTTP‑Stream und den zwei Architekturvarianten mit MCP besprochen haben, an einem durchgehenden GiftGenius‑Szenario – vom Nutzerwunsch bis zum fertigen Bericht.

Der Nutzer schreibt in ChatGPT: „Finde ein Geschenk für einen Fan von Brettspielen, Budget bis 100 US‑Dollar“. Das Modell entscheidet, GiftGenius aufzurufen. Die App/der Agent führt einen Tool‑Call start_gift_job auf Ihrem MCP‑Server aus. Der Server:

  • schreibt den Job in die Datenbank;
  • stellt ihn in eine interne Warteschlange (Details zu Queues und Workern – in der nächsten Vorlesung; vorerst nehmen wir an, dass „jemand“ ihn ausführt);
  • gibt synchron eine jobId als Antwort auf den Tool‑Call zurück.

Das GiftGenius‑Widget erhält ein ToolOutput mit jobId und rendert die Komponente:

function GiftGeniusRoot({ jobId }: { jobId: string }) {
  return (
    <div>
      <h2>Wir suchen die perfekten Geschenke...</h2>
      <GiftJobProgress jobId={jobId} />
      <GiftReport />
    </div>
  );
}

Die Komponente GiftJobProgress abonniert den SSE‑Endpoint /api/gift-jobs/{jobId}/events und zeichnet den Fortschritt. Jedes job.progress aktualisiert die Prozentwerte, job.completed setzt auf 100% und schaltet eventuell die Schaltfläche „Ausführlichen Bericht anzeigen“ frei.

Die Komponente GiftReport sendet beim Klick eine POST an /api/gift-report (unter Übergabe der jobId) und zeigt den Textbericht schrittweise an, während der Server die Chunks des HTTP‑Streams liefert.

Bei einem Abbruch der SSE‑Verbindung zeigt das Widget eine dezente Warnung, und EventSource versucht, sich neu zu verbinden. Bei Problemen mit dem Bericht‑Stream sieht der Nutzer den bereits empfangenen Teil und eine Schaltfläche „Berichtserstellung fortsetzen“ oder „Erneut versuchen“.

Aus Sicht von ChatGPT und MCP:

  • MCP sieht den Tool‑Aufruf start_gift_job und ggf. danach Benachrichtigungen über Job‑Status;
  • Der UX rund um Streams ist überwiegend auf der HTTP‑Ebene zwischen Widget und Ihrem Backend umgesetzt.

9. Häufige Fehler bei der Arbeit mit SSE und HTTP‑Stream

Fehler Nr. 1: SSE und HTTP‑Stream für „ein und dasselbe“ halten.
Ja, unten drunter ist beides HTTP mit chunked‑Antworten, aber die Semantik unterscheidet sich stark. SSE ist ein Abonnement unabhängiger Events, die jederzeit eintreffen können, und der Client weiß das vorher nicht. HTTP‑Stream ist eine konkrete Antwort, zeitlich gestreckt. Wenn Sie versuchen, ein Abonnement für viele jobId über einen einzigen HTTP‑Stream zu bauen, müssen Sie selbst ein Protokoll über Bytes erfinden – am Ende rekonstruieren Sie die Hälfte von SSE.

Fehler Nr. 2: Autoreconnect von SSE ignorieren und nicht über Idempotenz nachdenken.
Viele bauen einen „einfachen“ SSE‑Server: Sie senden data: ..., fügen aber weder das Standardfeld id: (für Last-Event-ID) noch eine anwendungsseitige event_id im Eventkörper hinzu. Beim ersten Verbindungsabbruch und Reconnect häufen sich dann doppelte Events. Ohne durchdachte event_id und „habe ich dieses Event schon gesehen?“‑Logik riskiert der Client, den Zustand doppelt zu aktualisieren, dasselbe job.completed zweimal zu zeigen oder – schlimmer – zweimal Geld abzubuchen/Bonus gutzuschreiben.

Fehler Nr. 3: Jedes Niesen des Workers als separates SSE‑Event senden.
Wenn Sie Fortschritt im Millisekundentakt per SSE schicken, killen Sie wahrscheinlich eher Netz und Client, als den Nutzer mit flüssiger Animation zu erfreuen. Viel sinnvoller ist es, Updates zu aggregieren und Fortschritt z. B. alle 200500 ms oder bei Phasenwechseln zu senden. Throttling und Backpressure sind eigene Themen, aber schon hier sollten Sie über die Eventfrequenz nachdenken.

Fehler Nr. 4: Komplexe Protokolle über HTTP‑Stream ohne klares Format aufbauen.
Ein typisches Anti‑Pattern: JSON ohne Trenner streamen und versuchen zu „erraten“, wo ein Objekt endet und das nächste beginnt. Oder Text und JSON im selben Stream vermischen. Der beste Weg: ein einfaches, klares Format wählen – Zeilentext, oder NDJSON (ein JSON‑Objekt pro Zeile), oder explizite Trenner. Dann bleibt der Parser auf dem Client überschaubar.

Fehler Nr. 5: Timeouts vergessen und „ewige“ Streams bauen.
Manchmal implementieren Entwickler SSE‑Endpoints, die 510 Minuten lang nichts senden, und wundern sich dann, dass Verbindungen unterwegs vom Nutzer bis zum Server abreißen (Load‑Balancer, API‑Gateways, Unternehmens‑Proxys). Regelmäßige Heartbeat‑Events oder Kommentare halten die Verbindung am Leben und helfen, Abbrüche rechtzeitig zu erkennen. Und HTTP‑Streams sollten nicht zu endlosen Antworten werden – für dauerhafte Abos gibt es SSE.

Fehler Nr. 6: Über HTTP‑Stream ein komplexes Pub/Sub statt sauberer Events bauen.
Mitunter lockt die Idee: „Wir machen einen Stream, darüber schicken wir Fortschritt, Partial Results und zufällige Logs.“ Am Ende entsteht auf dem Client ein komplexer Multiplexer, der jeden Chunk analysiert und entscheidet, zu welcher jobId er gehört. In den meisten Fällen ist es einfacher und zuverlässiger, SSE mit Eventtypen wie job.progress, job.completed und einem separaten Kanal pro Job zu verwenden, statt einen hausgemachten Megaprotokoll‑Stream zu erfinden.

Fehler Nr. 7: Den UX hart daran binden, dass der Stream „nie fällt“.
Jeder Stream reißt irgendwann ab. Wenn Ihr Widget dann mit einem ewig animierten Fortschrittsbalken ohne Optionen zurückbleibt – wirkt der UX „kaputt“. Schon eine einfache Meldung „Die Verbindung scheint unterbrochen. Bitte starten Sie die Geschenkfindung erneut“ mit einer „Erneut versuchen“‑Schaltfläche ist deutlich besser als Schweigen.

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION