Streaminganfragen mit der Fetch API

Jake Archibald
Jake Archibald

In Chromium 105 können Sie mithilfe der Streams API eine Anfrage starten, bevor Ihnen der gesamte Text zur Verfügung steht.

Damit können Sie:

  • Wärmen Sie den Server auf. Mit anderen Worten: Sie könnten die Anfrage starten, sobald der Nutzer den Fokus auf ein Texteingabefeld legt, alle Header wegräumen und dann warten, bis der Nutzer auf „Senden“ klickt. bevor die eingegebenen Daten gesendet werden.
  • Senden Sie nach und nach auf dem Client generierte Daten wie Audio-, Video- oder Eingabedaten.
  • Erstellen Sie WebSockets über HTTP/2 oder HTTP/3 neu.

Da es sich jedoch um eine Low-Level-Webplattformfunktion handelt, lassen Sie sich nicht durch meine Ideen einschränken. Vielleicht fällt Ihnen ein viel interessanterer Anwendungsfall für das Streaming von Anfragen ein.

Demo

Hier sehen Sie, wie Sie Daten vom Nutzer an den Server streamen und Daten zurücksenden können, die in Echtzeit verarbeitet werden können.

Ja, okay, das ist nicht das einfallsreichste Beispiel. Ich wollte es nur einfach halten, okay?

Wie auch immer, wie funktioniert das?

Spannende Abenteuer mit Abruf-Streams

Antwortstreams sind schon seit geraumer Zeit in allen modernen Browsern verfügbar. Sie ermöglichen es Ihnen, auf Teile einer Antwort zuzugreifen, sobald diese vom Server eintreffen:

const response = await fetch(url);
const reader = response.body.getReader();

while (true) {
  const {value, done} = await reader.read();
  if (done) break;
  console.log('Received', value);
}

console.log('Response fully received');

Jede value ist eine Uint8Array von Byte. Die Anzahl der abgerufenen Arrays und die Größe der Arrays hängen von der Netzwerkgeschwindigkeit ab. Bei einer schnellen Verbindung erhalten Sie weniger, aber größere „Blöcke“. von Daten. Bei einer langsamen Verbindung erhalten Sie mehr, kleinere Blöcke.

Wenn Sie die Byte in Text umwandeln möchten, können Sie TextDecoder oder den neueren Transformationsstream verwenden, sofern dies von Ihren Zielbrowsern unterstützt wird:

const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

TextDecoderStream ist ein Transformationsstream, der alle Uint8Array-Blöcke erfasst und in Strings konvertiert.

Streams sind großartig, da Sie sofort auf die Daten reagieren können, die sie erhalten. Wenn Sie beispielsweise eine Liste mit 100 Ergebnissen erhalten, können Sie das erste Ergebnis anzeigen, sobald Sie es erhalten, anstatt auf alle 100 warten zu müssen.

Das sind Antwort-Streams. Heute möchte ich Ihnen noch die Anfrage-Streams vorstellen.

Nachrichtentexte für Streaminganfragen

Anfragen können Text enthalten:

await fetch(url, {
  method: 'POST',
  body: requestBody,
});

Bisher musste der gesamte Körper einsatzbereit sein, bevor die Anfrage gestartet werden konnte. In Chromium 105 kannst du jetzt deine eigenen ReadableStream an Daten angeben:

function wait(milliseconds) {
  return new Promise(resolve => setTimeout(resolve, milliseconds));
}

const stream = new ReadableStream({
  async start(controller) {
    await wait(1000);
    controller.enqueue('This ');
    await wait(1000);
    controller.enqueue('is ');
    await wait(1000);
    controller.enqueue('a ');
    await wait(1000);
    controller.enqueue('slow ');
    await wait(1000);
    controller.enqueue('request.');
    controller.close();
  },
}).pipeThrough(new TextEncoderStream());

fetch(url, {
  method: 'POST',
  headers: {'Content-Type': 'text/plain'},
  body: stream,
  duplex: 'half',
});

Der obige Befehl sendet die Meldung „Dies ist eine langsame Anfrage“. Wort für Wort mit einer Sekunde Pause zwischen den einzelnen Wörtern an den Server.

Jeder Block eines Anfragetexts muss ein Uint8Array von Byte sein, also verwende ich pipeThrough(new TextEncoderStream()), um die Konvertierung für mich durchzuführen.

Einschränkungen

Streaminganfragen stellen eine neue Leistungsfähigkeit im Web dar, daher gelten für sie einige Einschränkungen:

Halbduplex?

Damit Streams in einer Anfrage verwendet werden können, muss die Anfrageoption duplex auf 'half' festgelegt werden.

Eine wenig bekannte Funktion von HTTP (ob dies das Standardverhalten ist, hängt davon ab, wen Sie fragen) besteht darin, dass Sie bereits die Antwort erhalten können, während Sie die Anfrage senden. Sie ist jedoch so wenig bekannt, dass sie weder von Servern noch von jedem Browser unterstützt wird.

In Browsern ist die Antwort erst verfügbar, wenn der Anfragetext vollständig gesendet wurde, auch wenn der Server die Antwort früher sendet. Dies gilt für alle Browserabrufe.

Dieses Standardmuster wird als „Halbduplex“ bezeichnet. Bei einigen Implementierungen, z. B. fetch in Deno, wird jedoch standardmäßig „Vollduplex“ verwendet. für Streaming-Abrufe, d. h. die Antwort kann verfügbar werden, bevor die Anfrage abgeschlossen ist.

Um dieses Kompatibilitätsproblem zu umgehen, muss duplex: 'half' in Browsern wird bei Anfragen mit Streamtext angegeben.

Zukünftig wird duplex: 'full' in Browsern möglicherweise für Streaming- und Nicht-Streaming-Anfragen unterstützt.

In der Zwischenzeit besteht die nächstbeste Möglichkeit zur Duplex-Kommunikation darin, einen Abruf mit einer Streaminganfrage durchzuführen und dann einen weiteren Abruf durchzuführen, um die Streaming-Antwort zu erhalten. Der Server benötigt eine Möglichkeit, diese beiden Anfragen zu verknüpfen, z. B. eine ID in der URL. So funktioniert die Demo.

Eingeschränkte Weiterleitungen

Bei einigen Formen der HTTP-Weiterleitung muss der Browser den Text der Anfrage noch einmal an eine andere URL senden. Um dies zu unterstützen, müsste der Browser den Inhalt des Streams zwischenspeichern. Dadurch wird der Punkt umgangen, sodass er das nicht tut.

Wenn die Anfrage einen Streaming-Text hat und die Antwort eine andere HTTP-Weiterleitung als 303 ist, wird der Abruf abgelehnt und der Weiterleitung nicht gefolgt.

303-Weiterleitungen sind zulässig, da sie die Methode explizit in GET ändern und den Anfragetext verwerfen.

Erfordert CORS und löst einen Preflight aus

Streaminganfragen haben einen Textkörper, aber keinen Content-Length-Header. Da dies eine neue Art von Anfrage ist, ist CORS erforderlich. Diese Anfragen lösen immer einen Preflight aus.

Streaminganfragen von no-cors sind nicht zulässig.

Funktioniert nicht unter HTTP/1.x

Der Abruf wird abgelehnt, wenn die Verbindung über HTTP/1.x erfolgt.

Dies liegt daran, dass gemäß HTTP/1.1-Regeln entweder der Anfrage- und Antworttext entweder einen Content-Length-Header senden muss, damit die andere Seite weiß, wie viele Daten er empfängt, oder das Format der Nachricht ändern muss, um die aufgeteilte Codierung zu verwenden. Bei der aufgeteilten Codierung wird der Textkörper in mehrere Teile aufgeteilt, die jeweils eine eigene Inhaltslänge haben.

Die Chunked-Codierung ist bei HTTP/1.1-Antworten ziemlich verbreitet, aber sehr selten bei Anfragen, daher stellt sie ein zu großes Kompatibilitätsrisiko dar.

Mögliche Probleme

Dies ist eine neue Funktion, die im Internet heute kaum genutzt wird. Achten Sie auf folgende Punkte:

Inkompatibilität auf Serverseite

Einige Anwendungsserver unterstützen keine Streaminganfragen und warten stattdessen, bis die vollständige Anfrage eingegangen ist, bevor Sie sie sehen. Damit ist der Zugangspunkt irgendwie zunichte geworden. Verwenden Sie stattdessen einen Anwendungsserver, der Streaming unterstützt, z. B. NodeJS oder Deno.

Aber du bist noch nicht aus dem Wald! Der Anwendungsserver wie NodeJS befindet sich normalerweise hinter einem anderen Server, der oft als „Frontend-Server“ bezeichnet wird, der wiederum hinter einem CDN stehen kann. Wenn einer dieser Vorgänge die Anfrage puffert, bevor sie an den nächsten Server in der Kette übergeben wird, verlieren Sie den Vorteil des Anfragestreamings.

Inkompatibilität außerhalb Ihrer Kontrolle

Da diese Funktion nur über HTTPS funktioniert, müssen Sie sich keine Gedanken über Proxys zwischen Ihnen und dem Nutzer machen. Möglicherweise führt der Nutzer jedoch einen Proxy auf seinem Computer aus. Einige Internet-Schutz-Software tut dies, um zu ermöglichen, dass sie alles zwischen dem Browser und dem Netzwerk überwachen kann, und es kann Fälle geben, in denen diese Software Anforderungstexte zwischenspeichert.

Wenn Sie sich davor schützen möchten, können Sie einen „Funktionstest“ erstellen. ähnlich wie in der Demo oben, in der Sie versuchen, einige Daten zu streamen, ohne den Stream zu schließen. Wenn der Server die Daten empfängt, kann er über einen anderen Abruf reagieren. Anschließend wissen Sie, dass der Client End-to-End-Streaminganfragen unterstützt.

Funktionserkennung

const supportsRequestStreams = (() => {
  let duplexAccessed = false;

  const hasContentType = new Request('', {
    body: new ReadableStream(),
    method: 'POST',
    get duplex() {
      duplexAccessed = true;
      return 'half';
    },
  }).headers.has('Content-Type');

  return duplexAccessed && !hasContentType;
})();

if (supportsRequestStreams) {
  // …
} else {
  // …
}

Falls Sie neugierig sind, erfahren Sie hier, wie die Funktionserkennung funktioniert:

Wenn der Browser einen bestimmten body-Typ nicht unterstützt, wird toString() für das Objekt aufgerufen und das Ergebnis als Text verwendet. Wenn der Browser Anfragestreams nicht unterstützt, wird der Anfragetext in den String "[object ReadableStream]". Wenn ein String als Text verwendet wird, wird der Content-Type-Header praktisch auf text/plain;charset=UTF-8 gesetzt. Wenn also dieser Header festgelegt ist, wissen wir, dass der Browser Streams in Anfrageobjekten nicht unterstützt, und kann vorzeitig beendet werden.

Safari unterstützt Streams in Anfrageobjekten, erlaubt jedoch nicht, dass sie mit fetch verwendet werden. Daher wird die Option duplex getestet, die Safari derzeit nicht unterstützt.

Verwendung mit beschreibbaren Streams

Manchmal ist es einfacher, mit Streams zu arbeiten, wenn du eine WritableStream hast. Dazu kannst du eine „Identität“ verwenden, stream. Dabei handelt es sich um ein lesbares/beschreibbares Paar, das alles, was an sein beschreibbares Ende übergeben wird, an das lesbare Ende sendet. Sie können einen dieser Parameter erstellen, indem Sie einen TransformStream ohne Argumente erstellen:

const {readable, writable} = new TransformStream();

const responsePromise = fetch(url, {
  method: 'POST',
  body: readable,
});

Jetzt ist alles, was Sie an den schreibbaren Stream senden, Teil der Anfrage. So kannst du Streams gemeinsam erstellen. Hier ist ein Beispiel, bei dem Daten von einer URL abgerufen, komprimiert und an eine andere URL gesendet werden:

// Get from url1:
const response = await fetch(url1);
const {readable, writable} = new TransformStream();

// Compress the data from url1:
response.body.pipeThrough(new CompressionStream('gzip')).pipeTo(writable);

// Post to url2:
await fetch(url2, {
  method: 'POST',
  body: readable,
});

Im obigen Beispiel werden Komprimierungsstreams verwendet, um beliebige Daten mit gzip zu komprimieren.