Vanaf Chromium 105 kunt u een aanvraag starten voordat de volledige body beschikbaar is, door de Streams API te gebruiken.
Je kunt dit gebruiken om:
- Warm de server op. Met andere woorden, je zou de aanvraag kunnen starten zodra de gebruiker een tekstinvoerveld heeft geselecteerd, alle headers verwijderen en vervolgens wachten tot de gebruiker op 'Verzenden' drukt voordat je de ingevoerde gegevens verstuurt.
- Verstuur geleidelijk de gegevens die op de client zijn gegenereerd, zoals audio, video of invoergegevens.
- Websockets opnieuw aanmaken via HTTP/2 of HTTP/3.
Maar aangezien dit een low-level webplatformfunctie is, laat je niet beperken door mijn ideeën. Misschien kun je wel een veel spannender gebruiksvoorbeeld bedenken voor request streaming.
Demonstratie
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 werkt dit nu eigenlijk?
Eerder over de spannende avonturen van het ophalen van streams
Reactiestromen zijn al een tijdje beschikbaar in alle moderne browsers. Hiermee kunt u delen van een reactie bekijken zodra deze van de server komen:
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 je krijgt en de grootte ervan zijn afhankelijk van de snelheid van het netwerk. Als je een snelle verbinding hebt, ontvang je minder, grotere 'brokken' data. Als je een langzame verbinding hebt, ontvang je meer, kleinere brokken.
Als u de bytes naar tekst wilt converteren, kunt u TextDecoder
gebruiken of de nieuwere transform stream als uw doelbrowser dit ondersteunt :
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
is een transformatiestroom die alle Uint8Array
fragmenten pakt en ze omzet in strings.
Streams zijn geweldig, omdat je direct actie kunt ondernemen met de data die binnenkomt. Als je bijvoorbeeld een lijst met 100 'resultaten' ontvangt, kun je het eerste resultaat direct weergeven, in plaats van te wachten op alle 100.
Hoe dan ook, dit zijn responsstromen. Het nieuwe interessante waar ik het over wilde hebben zijn verzoekstromen.
Streaming-aanvraaglichamen
Verzoeken kunnen de volgende inhoud hebben:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Voorheen moest het hele lichaam klaar zijn voordat je de aanvraag kon starten, maar nu kun je in Chromium 105 je eigen ReadableStream
met gegevens opgeven:
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',
});
Met bovenstaande opdracht wordt de tekst "Dit is een langzaam verzoek" naar de server verzonden, één woord per keer, met een pauze van één seconde tussen elk woord.
Elk deel van de aanvraagbody moet een Uint8Array
van bytes zijn. Daarom gebruik ik pipeThrough(new TextEncoderStream())
om de conversie voor mij uit te voeren.
Beperkingen
Streamingverzoeken zijn een nieuwe kracht op het web en hebben daarom een aantal beperkingen:
Half-duplex?
Om streams in een aanvraag te kunnen gebruiken, moet de optie voor duplex
worden ingesteld op 'half'
.
Een weinig bekende functie van HTTP (hoewel het afhangt van wie je het vraagt of dit standaardgedrag is) is dat je de respons al kunt ontvangen terwijl je nog bezig bent met het versturen van de aanvraag. Deze functie is echter zo weinig bekend dat het niet goed wordt ondersteund door servers en door geen enkele browser.
In browsers is het antwoord pas beschikbaar nadat de aanvraag volledig is verzonden, zelfs als de server eerder een antwoord stuurt. Dit geldt voor alle gegevens die in de browser worden opgehaald.
Dit standaardpatroon staat bekend als 'half-duplex'. Sommige implementaties, zoals fetch
in Deno , gebruikten echter standaard 'full-duplex' voor streaming fetches, wat betekent dat de respons beschikbaar kan zijn voordat de aanvraag is voltooid.
Om dit compatibiliteitsprobleem te omzeilen, moet in browsers duplex: 'half'
worden opgegeven bij aanvragen met een stream body.
In de toekomst wordt duplex: 'full'
mogelijk ondersteund in browsers voor streaming- en niet-streamingaanvragen.
In de tussentijd is de beste manier om duplexcommunicatie te gebruiken, één fetch te maken met een streamingverzoek en vervolgens nog een fetch te maken om de streamingrespons 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
Sommige vormen van HTTP-redirect vereisen dat de browser de hoofdtekst van het verzoek opnieuw naar een andere URL stuurt. Om dit te ondersteunen, zou de browser de inhoud van de stream moeten bufferen, wat het doel enigszins tenietdoet, dus dat gebeurt niet.
Als de aanvraag echter een streaming body heeft en het antwoord een andere HTTP-omleiding is dan 303, wordt de opvraging afgewezen en wordt de omleiding niet gevolgd.
303-omleidingen zijn toegestaan, omdat ze de methode expliciet wijzigen naar GET
en de aanvraagtekst negeren.
Vereist CORS en activeert een preflight
Streamingverzoeken hebben een body, maar geen Content-Length
-header. Dat is een nieuw type 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 geweigerd als de verbinding HTTP/1.x is.
Dit komt doordat, volgens de HTTP/1.1-regels, de aanvraag- en antwoordbody's ofwel een Content-Length
header moeten versturen, zodat de andere partij weet hoeveel data er zal worden ontvangen, ofwel de opmaak van het bericht moeten aanpassen om chunked encoding te gebruiken. Bij chunked encoding wordt de body in delen gesplitst, elk met een eigen contentlengte.
Chunked encoding komt vrij vaak voor bij HTTP/1.1 -reacties , maar komt zelden voor bij verzoeken . Er is dus sprake van een te groot compatibiliteitsrisico.
Mogelijke problemen
Dit is een nieuwe functie die momenteel nog maar weinig wordt benut op internet. Hier zijn enkele aandachtspunten:
Incompatibiliteit aan de serverzijde
Sommige app-servers ondersteunen geen streamingverzoeken en wachten in plaats daarvan tot het volledige verzoek is ontvangen voordat ze je er iets van laten zien, wat het doel enigszins tenietdoet. Gebruik in plaats daarvan een app-server die streaming ondersteunt, zoals NodeJS of Deno .
Maar je bent er nog niet! De applicatieserver, zoals NodeJS, bevindt zich meestal achter een andere server, vaak een "front-end server" genoemd, die op zijn beurt weer achter een CDN kan staan. Als een van deze servers besluit het verzoek te bufferen voordat het naar de volgende server in de keten wordt doorgestuurd, verlies je het voordeel van request streaming.
Onverenigbaarheid buiten uw controle
Omdat deze functie alleen via HTTPS werkt, hoeft u zich geen zorgen te maken over proxyservers tussen u en de gebruiker. De gebruiker gebruikt mogelijk wel een proxyserver op zijn of haar computer. Sommige internetbeveiligingssoftware doet dit om alles wat er tussen de browser en het netwerk gebeurt te kunnen monitoren. In sommige gevallen kan deze software de body van verzoeken bufferen.
Als u zich hiertegen wilt beschermen, kunt u een 'feature test' maken, vergelijkbaar met de demo hierboven , waarbij u probeert gegevens te streamen zonder de stream te sluiten. Als de server de gegevens ontvangt, kan deze via een andere fetch 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 {
// …
}
Bent u nieuwsgierig? Hier leest u hoe de functiedetectie werkt:
Als de browser een bepaald body
niet ondersteunt, wordt toString()
aangeroepen voor het object en wordt het resultaat als body gebruikt. Dus als de browser geen request streams ondersteunt, wordt de request body de string "[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 requestobjecten ondersteunt en kunnen we vroegtijdig stoppen.
Safari ondersteunt wel streams in aanvraagobjecten, maar staat het gebruik ervan met fetch
niet toe. Daarom wordt de duplex
getest. Safari ondersteunt deze momenteel niet.
Gebruik met schrijfbare streams
Soms is het makkelijker om met streams te werken als je een WritableStream
hebt. Je kunt dit doen met een 'identity'-stream, een leesbaar/schrijfbaar paar dat alles wat naar het schrijfbare einde wordt gestuurd, naar het leesbare einde stuurt. Je kunt er een maken door een TransformStream
zonder argumenten te maken:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Alles wat je naar de schrijfbare stream stuurt, maakt nu deel uit van de aanvraag. Zo kun je streams samenvoegen. Hier is bijvoorbeeld een grappig voorbeeld waarbij gegevens van de ene URL worden opgehaald, 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.