Streamingverzoeken met de fetch-API

Vanaf Chromium 105 kun je een verzoek starten voordat je de hele body beschikbaar hebt door gebruik te maken van de Streams API .

Je zou dit kunnen gebruiken om:

  • Warm de server op. Met andere woorden, u kunt het verzoek starten zodra de gebruiker de focus op een tekstinvoerveld heeft gelegd en alle kopteksten uit de weg halen, en vervolgens wachten tot de gebruiker op 'verzenden' drukt voordat hij de ingevoerde gegevens verzendt.
  • Verzend geleidelijk gegevens die op de client zijn gegenereerd, zoals audio-, video- of invoergegevens.
  • Creëer websockets opnieuw via HTTP/2 of HTTP/3.

Maar aangezien dit een webplatformfunctie op laag niveau is, laat u zich niet beperken door mijn ideeën. Misschien kun je een veel spannender gebruiksscenario bedenken voor het streamen van verzoeken.

Demo

Dit laat zien hoe u gegevens van de gebruiker naar de server kunt streamen en gegevens terug kunt sturen die in realtime kunnen worden verwerkt.

Ja, oké, het is niet het meest fantasierijke voorbeeld, ik wilde het gewoon simpel houden, oké?

Hoe dan ook, hoe werkt dit?

Eerder over de spannende avonturen van ophaalstreams

Reactiestreams zijn al een tijdje beschikbaar in alle moderne browsers. Hiermee kunt u toegang krijgen tot delen van een antwoord zodra deze van de server binnenkomen:

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');

Elke value is een Uint8Array van bytes. Het aantal arrays dat u krijgt en de grootte van de arrays zijn afhankelijk van de snelheid van het netwerk. Als u een snelle verbinding heeft, ontvangt u minder, maar grotere 'brokken' gegevens. Als je een langzame verbinding hebt, krijg je meer, kleinere stukjes.

Als u de bytes naar tekst wilt converteren, kunt u TextDecoder gebruiken, of de nieuwere transformatiestroom als uw doelbrowser dit ondersteunt :

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

TextDecoderStream is een transformatiestroom die al die Uint8Array brokken oppakt en omzet naar strings.

Streams zijn geweldig, omdat u kunt reageren op de gegevens zodra deze binnenkomen. Als u bijvoorbeeld een lijst met 100 'resultaten' ontvangt, kunt u het eerste resultaat weergeven zodra u het ontvangt, in plaats van te wachten op alle 100.

Hoe dan ook, dat zijn responsstreams, het opwindende nieuwe waar ik het over wilde hebben zijn verzoekstreams.

Streamingverzoekinstanties

Verzoeken kunnen de volgende inhoud hebben:

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

Voorheen moest het hele lichaam gereed zijn voordat u het verzoek kon starten, maar nu kunt u in Chromium 105 uw eigen ReadableStream aan gegevens leveren:

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',
});

Het bovenstaande stuurt "Dit is een langzaam verzoek" naar de server, woord voor woord, met een pauze van één seconde tussen elk woord.

Elk deel van de hoofdtekst van een verzoek moet een Uint8Array van bytes zijn, dus ik gebruik pipeThrough(new TextEncoderStream()) om de conversie voor mij uit te voeren.

Beperkingen

Streamingverzoeken zijn een nieuwe kracht voor internet en brengen daarom enkele beperkingen met zich mee:

Half duplex?

Om het gebruik van streams in een verzoek mogelijk te maken, moet de duplex op 'half' worden ingesteld.

Een weinig bekende functie van HTTP (hoewel of dit standaardgedrag is, hangt af van wie u het vraagt) is dat u het antwoord kunt ontvangen terwijl u het verzoek nog verzendt. Het is echter zo weinig bekend dat het niet goed wordt ondersteund door servers en door geen enkele browser wordt ondersteund.

In browsers komt het antwoord pas beschikbaar als de verzoektekst volledig is verzonden, zelfs als de server eerder een antwoord verzendt. Dit geldt voor alle browserophaalbewerkingen.

Dit standaardpatroon staat bekend als 'half duplex'. Sommige implementaties, zoals fetch in Deno , zijn echter standaard ingesteld op 'full duplex' voor streaming-fetches, wat betekent dat het antwoord beschikbaar kan komen voordat het verzoek is voltooid.

Om dit compatibiliteitsprobleem te omzeilen, moet duplex: 'half' in browsers worden opgegeven bij verzoeken met een streambody.

In de toekomst duplex: 'full' mogelijk ondersteund in browsers voor streaming- en niet-streamingverzoeken.

In de tussentijd is het beste alternatief voor duplexcommunicatie het uitvoeren van één ophaalactie met een streamingverzoek en vervolgens nog een ophaalactie om het streamingantwoord te ontvangen. De server heeft een manier nodig om deze twee verzoeken te koppelen, zoals een ID in de URL. Zo werkt de demo .

Beperkte omleidingen

Bij sommige vormen van HTTP-omleiding moet de browser de hoofdtekst van het verzoek opnieuw naar een andere URL verzenden. Om dit te ondersteunen zou de browser de inhoud van de stream moeten bufferen, wat het punt min of meer tenietdoet, dus dat doet hij niet.

Als het verzoek een streaming-body heeft en het antwoord een andere HTTP-omleiding dan 303 is, wordt het ophalen afgewezen en wordt de omleiding niet gevolgd.

303-omleidingen zijn toegestaan, omdat ze de methode expliciet wijzigen in GET en de verzoektekst negeren.

Vereist CORS en activeert een preflight

Streamingverzoeken hebben een hoofdtekst, maar geen Content-Length header. Dat is een nieuw soort verzoek, dus CORS is vereist, en deze verzoeken activeren altijd een preflight.

Streaming no-cors verzoeken zijn niet toegestaan.

Werkt niet op HTTP/1.x

Het ophalen wordt afgewezen als de verbinding HTTP/1.x is.

Dit komt omdat, volgens de HTTP/1.1-regels, de aanvraag- en responsinstanties ofwel een Content-Length -header moeten verzenden, zodat de andere partij weet hoeveel gegevens deze zal ontvangen, ofwel de indeling van het bericht moeten wijzigen om chunked-codering te gebruiken. Met gefragmenteerde codering wordt de body in delen opgesplitst, elk met hun eigen inhoudslengte.

Gefragmenteerde codering is vrij gebruikelijk als het gaat om HTTP/1.1 -reacties , maar zeer zeldzaam als het om verzoeken gaat, dus het is een te groot compatibiliteitsrisico.

Mogelijke problemen

Dit is een nieuwe functie, die momenteel nog te weinig wordt gebruikt op internet. Hier zijn enkele problemen waar u op moet letten:

Incompatibiliteit aan de serverzijde

Sommige app-servers ondersteunen geen streamingverzoeken en wachten in plaats daarvan tot het volledige verzoek is ontvangen voordat je er iets van kunt zien, wat het punt eigenlijk teniet doet. Gebruik in plaats daarvan een app-server die streaming ondersteunt, zoals NodeJS of Deno .

Maar je bent nog niet uit het bos! De applicatieserver, zoals NodeJS, bevindt zich meestal achter een andere server, ook wel een "front-end server" genoemd, die op zijn beurt weer achter een CDN kan zitten. Als een van hen besluit het verzoek te bufferen voordat het aan de volgende server in de keten wordt doorgegeven, verliest u het voordeel van het streamen van verzoeken.

Incompatibiliteit buiten uw controle

Omdat deze functie alleen via HTTPS werkt, hoeft u zich geen zorgen te maken over proxy's tussen u en de gebruiker, maar de gebruiker voert mogelijk een proxy uit op zijn computer. Sommige internetbeveiligingssoftware doet dit om alles te kunnen monitoren wat er tussen de browser en het netwerk gebeurt, en er kunnen gevallen zijn waarin deze software verzoekinstanties buffert.

Als je je hiertegen wilt beschermen, kun je een 'feature test' maken, vergelijkbaar met de demo hierboven , waarbij je wat gegevens probeert te streamen zonder de stream te sluiten. Als de server de gegevens ontvangt, kan deze via een andere ophaalactie reageren. Zodra dit gebeurt, weet u dat de client streamingverzoeken end-to-end ondersteunt.

Functiedetectie

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 {
  // …
}

Als je nieuwsgierig bent, kun je hier zien hoe de functiedetectie werkt:

Als de browser een bepaald body type niet ondersteunt, roept hij toString() op voor het object en gebruikt het resultaat als body. Dus als de browser geen verzoekstreams ondersteunt, wordt de hoofdtekst van het verzoek de tekenreeks "[object ReadableStream]" . Wanneer een string als body wordt gebruikt, wordt de Content-Type header handig ingesteld op text/plain;charset=UTF-8 . Dus als die header is ingesteld, weten we dat de browser geen streams in verzoekobjecten ondersteunt, en kunnen we vroegtijdig afsluiten.

Safari ondersteunt wel streams in verzoekobjecten, maar staat niet toe dat deze worden gebruikt met fetch , dus wordt de duplex getest, die Safari momenteel niet ondersteunt.

Gebruik met beschrijfbare streams

Soms is het gemakkelijker om met streams te werken als je een WritableStream hebt. Je kunt dit doen met behulp van een 'identiteits'-stream, een leesbaar/schrijfbaar paar dat alles wat naar het schrijfbare einde wordt doorgegeven, naar het leesbare einde stuurt. U kunt een van deze maken door een TransformStream zonder argumenten te maken:

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

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

Nu zal alles wat u naar de beschrijfbare stream verzendt, deel uitmaken van het verzoek. Hierdoor kun je samen streams samenstellen. Hier is bijvoorbeeld een dom voorbeeld waarbij gegevens worden opgehaald van de ene URL, gecomprimeerd en naar een andere URL worden verzonden:

// 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,
});

In het bovenstaande voorbeeld worden compressiestromen gebruikt om willekeurige gegevens te comprimeren met behulp van gzip.