Ab Chromium 105 können Sie eine Anfrage starten, bevor der gesamte Textkörper verfügbar ist. Verwenden Sie dazu die Streams API.
Sie können damit Folgendes tun:
- Wärmen Sie den Server auf. Sie könnten die Anfrage also starten, sobald der Nutzer ein Texteingabefeld fokussiert, alle Header ausblenden und dann warten, bis der Nutzer auf „Senden“ drückt, bevor Sie die eingegebenen Daten senden.
- Senden Sie nach und nach Daten, die auf dem Client generiert werden, z. B. Audio-, Video- oder Eingabedaten.
- Websockets über HTTP/2 oder HTTP/3 neu erstellen
Da es sich jedoch um eine Webplattformfunktion auf niedriger Ebene handelt, sollten Sie sich nicht auf meine Ideen beschränken. Vielleicht fällt Ihnen ein viel spannenderer Anwendungsfall für das Streaming von Anfragen ein.
Bisherige spannende Abenteuer von Fetch-Streams
Antwortstreams sind schon seit einiger Zeit in allen modernen Browsern verfügbar. Sie ermöglichen den Zugriff auf Teile einer Antwort, sobald diese vom Server eingehen:
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');
Jeder value
ist ein Uint8Array
von Byte.
Die Anzahl und Größe der Arrays hängt von der Geschwindigkeit des Netzwerks ab.
Bei einer schnellen Verbindung erhalten Sie weniger, dafür aber größere Datenblöcke.
Bei einer langsamen Verbindung erhalten Sie mehr, kleinere Chunks.
Wenn Sie die Bytes in Text umwandeln möchten, können Sie TextDecoder
oder den neueren Transform-Stream verwenden, sofern Ihre Zielbrowser ihn unterstützen:
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
ist ein Transformationsstream, der alle Uint8Array
-Chunks abruft und in Strings konvertiert.
Streams sind sehr nützlich, da Sie auf die Daten reagieren können, sobald sie eintreffen. Wenn Sie beispielsweise eine Liste mit 100 Ergebnissen erhalten, können Sie das erste Ergebnis sofort anzeigen, anstatt auf alle 100 zu warten.
Das waren also Antwortstreams. Das spannende Neue, über das ich sprechen wollte, sind Anfragestreams.
Streaminganfragetexte
Anfragen können Textkörper haben:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Bisher mussten Sie den gesamten Textkörper vorbereiten, bevor Sie die Anfrage starten konnten. In Chromium 105 können Sie jetzt Ihre eigenen ReadableStream
an Daten bereitstellen:
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',
});
Mit dem oben genannten Befehl wird „This is a slow request“ (Das ist eine langsame Anfrage) an den Server gesendet, jeweils ein Wort nach dem anderen, mit einer Sekunde Pause zwischen den Wörtern.
Jeder Chunk eines Anfragetexts muss Uint8Array
Byte groß sein. Daher verwende ich pipeThrough(new TextEncoderStream())
, um die Konvertierung für mich durchzuführen.
Einschränkungen
Streaminganfragen sind eine neue Funktion für das Web und unterliegen daher einigen Einschränkungen:
Halbduplex?
Damit Streams in einer Anfrage verwendet werden können, muss die Anfrageoption duplex
auf 'half'
gesetzt sein.
Eine wenig bekannte Funktion von HTTP (obwohl das Standardverhalten davon abhängt, wen Sie fragen) ist, dass Sie die Antwort empfangen können, während Sie die Anfrage noch senden. Es ist jedoch so wenig bekannt, dass es von Servern und Browsern nicht 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. Das gilt für alle Browserabrufe.
Dieses Standardmuster wird als „Halbduplex“ bezeichnet.
Bei einigen Implementierungen, z. B. fetch
in Deno, wurde für Streaming-Abrufe jedoch standardmäßig „Vollduplex“ verwendet. Das bedeutet, dass die Antwort verfügbar sein kann, bevor die Anfrage abgeschlossen ist.
Um dieses Kompatibilitätsproblem zu umgehen, muss duplex: 'half'
in Browsern für Anfragen mit einem Stream-Body angegeben werden.
In Zukunft wird duplex: 'full'
möglicherweise in Browsern für Streaming- und Nicht-Streaming-Anfragen unterstützt.
Bis dahin ist die nächstbeste Möglichkeit, eine Anfrage mit Streaming zu senden und dann eine weitere Anfrage zu senden, um die Streaming-Antwort zu erhalten. Der Server muss die beiden Anfragen irgendwie zuordnen können, z. B. über eine ID in der URL. So funktioniert die Demo.
Eingeschränkte Weiterleitungen
Bei einigen Formen der HTTP-Weiterleitung muss der Browser den Textkörper der Anfrage an eine andere URL senden. Dazu müsste der Browser die Inhalte des Streams puffern, was den Sinn der Sache zunichtemacht. Daher wird das nicht gemacht.
Wenn die Anfrage einen Streaming-Body hat und die Antwort eine HTTP-Weiterleitung ist, die nicht 303 lautet, wird der Fetch abgelehnt und der Weiterleitung wird nicht gefolgt.
303-Weiterleitungen sind zulässig, da die Methode explizit in GET
geändert und der Anfragetext verworfen wird.
Erfordert CORS und löst einen Preflight aus
Streaming-Anfragen haben einen Textkörper, aber keinen Content-Length
-Header.
Das ist eine neue Art von Anfrage. Daher ist CORS erforderlich und diese Anfragen lösen immer einen Preflight aus.
Streaming-Anfragen für no-cors
sind nicht zulässig.
Funktioniert nicht mit HTTP/1.x
Der Abruf wird abgelehnt, wenn die Verbindung HTTP/1.x ist.
Das liegt daran, dass gemäß den HTTP/1.1-Regeln für Anfragen- und Antworttextkörper entweder ein Content-Length
-Header gesendet werden muss, damit die andere Seite weiß, wie viele Daten sie empfängt, oder das Format der Nachricht in Chunked Encoding geändert werden muss. Bei der Chunked Encoding-Methode wird der Hauptteil in Teile unterteilt, die jeweils eine eigene Inhaltslänge haben.
Die Chunked-Codierung ist bei HTTP/1.1-Antworten recht häufig, bei Anfragen jedoch sehr selten. Daher ist das Kompatibilitätsrisiko zu hoch.
Potenzielle Probleme
Dies ist eine neue Funktion, die im Internet derzeit noch wenig genutzt wird. Achten Sie auf Folgendes:
Inkompatibilität auf der Serverseite
Einige App-Server unterstützen keine Streaminganfragen und warten stattdessen, bis die vollständige Anfrage empfangen wurde, bevor sie Ihnen etwas davon anzeigen. Das ist natürlich nicht Sinn der Sache. Verwenden Sie stattdessen einen App-Server, der Streaming unterstützt, z. B. NodeJS oder Deno.
Aber du bist noch nicht über den Berg! Der Anwendungsserver, z. B. NodeJS, befindet sich in der Regel hinter einem anderen Server, der oft als „Frontend-Server“ bezeichnet wird und sich wiederum hinter einem CDN befinden kann. Wenn einer dieser Server die Anfrage puffert, bevor er sie an den nächsten Server in der Kette weiterleitet, geht der Vorteil des Anfragestreamings verloren.
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. Der Nutzer verwendet jedoch möglicherweise einen Proxy auf seinem Computer. Einige Internet-Sicherheitssoftware macht dies, um alles überwachen zu können, was zwischen dem Browser und dem Netzwerk übertragen wird. In manchen Fällen puffert diese Software Anfragetextkörper.
Wenn Sie sich davor schützen möchten, können Sie einen „Feature-Test“ ähnlich der Demo oben erstellen, bei dem 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 antworten. Wenn das passiert, wissen Sie, dass der Client Streaminganfragen vollständig 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 {
// …
}
So funktioniert die Funktion:
Wenn der Browser einen bestimmten body
-Typ nicht unterstützt, ruft er toString()
für das Objekt auf und verwendet das Ergebnis als Hauptteil.
Wenn der Browser keine Anfragestreams unterstützt, wird der Anfragetext zum String "[object ReadableStream]"
.
Wenn ein String als Body verwendet wird, wird der Content-Type
-Header automatisch auf text/plain;charset=UTF-8
gesetzt.
Wenn dieser Header festgelegt ist, wissen wir, dass der Browser Streams in Anfrageobjekten nicht unterstützt, und wir können den Vorgang frühzeitig beenden.
Safari unterstützt Streams in Anfrageobjekten, erlaubt aber nicht, dass sie mit fetch
verwendet werden. Daher wird die Option duplex
getestet, die Safari derzeit nicht unterstützt.
Mit beschreibbaren Streams verwenden
Manchmal ist es einfacher, mit Streams zu arbeiten, wenn Sie ein WritableStream
haben.
Dazu können Sie einen „Identitäts“-Stream verwenden. Das ist ein Lese-/Schreibpaar, das alles, was an das Schreibende übergeben wird, an das Lesende sendet.
Sie können ein solches Objekt erstellen, indem Sie ein TransformStream
-Objekt ohne Argumente erstellen:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Alles, was Sie an den beschreibbaren Stream senden, ist nun Teil der Anfrage. So können Sie Streams zusammenstellen. Hier ist ein einfaches 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.