À partir de Chromium 105, vous pouvez lancer une requête avant d'avoir le corps entier disponible en utilisant l'API Streams.
Cette fonctionnalité vous offre les possibilités suivantes :
- Préchauffez le serveur. En d'autres termes, vous pouvez lancer la requête une fois que l'utilisateur se concentre sur un champ de saisie de texte, puis supprimer tous les en-têtes. Ensuite, attendez que l'utilisateur appuie sur "Envoyer" avant d'envoyer les données qu'il a saisies.
- Envoyez progressivement les données générées sur le client, telles que les données audio, vidéo ou d'entrée.
- Recréez les WebSockets sur HTTP/2 ou HTTP/3.
Mais comme il s'agit d'une fonctionnalité de plate-forme Web de bas niveau, ne vous limitez pas à mes idées. Vous pouvez peut-être imaginer un cas d'utilisation beaucoup plus intéressant pour le streaming de requêtes.
Précédemment dans les aventures palpitantes des flux Fetch
Les flux de réponses sont disponibles dans tous les navigateurs modernes depuis un certain temps. Ils vous permettent d'accéder à des parties d'une réponse à mesure qu'elles arrivent du serveur :
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');
Chaque value
est un Uint8Array
d'octets.
Le nombre et la taille des tableaux dépendent de la vitesse du réseau.
Si votre connexion est rapide, vous recevrez moins de blocs de données, mais ils seront plus volumineux.
Si votre connexion est lente, vous recevrez des blocs plus petits et plus nombreux.
Si vous souhaitez convertir les octets en texte, vous pouvez utiliser TextDecoder
ou le nouveau flux de transformation si vos navigateurs cibles le prennent en charge :
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
est un flux de transformation qui récupère tous ces blocs Uint8Array
et les convertit en chaînes.
Les flux sont très utiles, car vous pouvez commencer à agir sur les données dès qu'elles arrivent. Par exemple, si vous recevez une liste de 100 résultats, vous pouvez afficher le premier dès que vous le recevez, au lieu d'attendre de les avoir tous.
Quoi qu'il en soit, ce sont les flux de réponse. La nouveauté passionnante dont je voulais vous parler, ce sont les flux de requête.
Corps des requêtes en flux continu
Les requêtes peuvent avoir des corps :
await fetch(url, {
method: 'POST',
body: requestBody,
});
Auparavant, vous deviez préparer l'intégralité du corps avant de pouvoir lancer la requête. Désormais, dans Chromium 105, vous pouvez fournir votre propre ReadableStream
de données :
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',
});
La commande ci-dessus enverra "This is a slow request" (Ceci est une requête lente) au serveur, un mot à la fois, avec une pause d'une seconde entre chaque mot.
Chaque bloc du corps de la requête doit être un Uint8Array
d'octets. J'utilise donc pipeThrough(new TextEncoderStream())
pour effectuer la conversion.
Restrictions
Les requêtes de streaming sont une nouvelle fonctionnalité pour le Web. Elles sont donc soumises à quelques restrictions :
Son unidirectionnel ?
Pour autoriser l'utilisation de flux dans une requête, l'option de requête duplex
doit être définie sur 'half'
.
Une fonctionnalité peu connue de HTTP (bien que le comportement standard dépende de la personne à qui vous demandez) est que vous pouvez commencer à recevoir la réponse pendant que vous envoyez encore la requête. Toutefois, il est si peu connu qu'il n'est pas bien pris en charge par les serveurs et qu'aucun navigateur ne le prend en charge.
Dans les navigateurs, la réponse n'est jamais disponible tant que le corps de la requête n'a pas été entièrement envoyé, même si le serveur envoie une réponse plus tôt. Cela s'applique à toutes les récupérations de navigateur.
Ce schéma par défaut est appelé "semi-duplex".
Toutefois, certaines implémentations, telles que fetch
dans Deno, ont été définies par défaut sur "full duplex" pour les récupérations de flux, ce qui signifie que la réponse peut devenir disponible avant la fin de la requête.
Pour contourner ce problème de compatibilité, dans les navigateurs, duplex: 'half'
doit être spécifié dans les requêtes qui comportent un corps de flux.
À l'avenir, duplex: 'full'
pourra être compatible avec les navigateurs pour les requêtes en streaming et sans streaming.
En attendant, la meilleure solution pour remplacer la communication en duplex consiste à effectuer une récupération avec une requête de streaming, puis à effectuer une autre récupération pour recevoir la réponse de streaming. Le serveur aura besoin d'un moyen d'associer ces deux requêtes, comme un ID dans l'URL. C'est ainsi que fonctionne la démonstration.
Redirections restreintes
Certaines formes de redirection HTTP exigent que le navigateur renvoie le corps de la requête à une autre URL. Pour ce faire, le navigateur devrait mettre en mémoire tampon le contenu du flux, ce qui n'a pas vraiment de sens. Il ne le fait donc pas.
En revanche, si la requête comporte un corps de flux et que la réponse est une redirection HTTP autre que 303, la récupération sera rejetée et la redirection ne sera pas suivie.
Les redirections 303 sont autorisées, car elles modifient explicitement la méthode en GET
et suppriment le corps de la requête.
Nécessite CORS et déclenche une requête préliminaire
Les requêtes de flux ont un corps, mais pas d'en-tête Content-Length
.
Il s'agit d'un nouveau type de requête. CORS est donc obligatoire, et ces requêtes déclenchent toujours une pré-vérification.
Les demandes de streaming no-cors
ne sont pas autorisées.
Ne fonctionne pas sur HTTP/1.x
La récupération sera refusée si la connexion est HTTP/1.x.
En effet, selon les règles HTTP/1.1, les corps de requête et de réponse doivent envoyer un en-tête Content-Length
pour que l'autre côté sache la quantité de données qu'il recevra, ou modifier le format du message pour utiliser l'encodage par blocs. Avec l'encodage par blocs, le corps est divisé en plusieurs parties, chacune ayant sa propre longueur de contenu.
L'encodage par blocs est assez courant pour les réponses HTTP/1.1, mais très rare pour les requêtes. Il présente donc un risque de compatibilité trop élevé.
Problèmes éventuels
Il s'agit d'une nouvelle fonctionnalité, qui est encore sous-utilisée sur Internet aujourd'hui. Voici quelques problèmes à éviter :
Incompatibilité côté serveur
Certains serveurs d'applications ne sont pas compatibles avec les requêtes de streaming et attendent que la requête complète soit reçue avant de vous en montrer une partie, ce qui est un peu dommage. Utilisez plutôt un serveur d'application compatible avec le streaming, comme NodeJS ou Deno.
Mais vous n'êtes pas encore sorti d'affaire ! Le serveur d'application, tel que NodeJS, se trouve généralement derrière un autre serveur, souvent appelé "serveur frontend", qui peut lui-même se trouver derrière un CDN. Si l'un d'eux décide de mettre en mémoire tampon la requête avant de la transmettre au serveur suivant de la chaîne, vous perdez l'avantage du streaming des requêtes.
Incompatibilité indépendante de votre volonté
Étant donné que cette fonctionnalité ne fonctionne qu'avec HTTPS, vous n'avez pas à vous soucier des proxys entre vous et l'utilisateur, mais il est possible que l'utilisateur exécute un proxy sur sa machine. Certains logiciels de protection Internet procèdent ainsi pour pouvoir surveiller tout ce qui transite entre le navigateur et le réseau. Il peut arriver que ce logiciel mette en mémoire tampon les corps de requête.
Pour vous en prémunir, vous pouvez créer un "test de fonctionnalité" semblable à la démonstration ci-dessus, dans lequel vous essayez de diffuser des données sans fermer le flux. Si le serveur reçoit les données, il peut répondre via une autre récupération. Une fois cette opération effectuée, vous savez que le client accepte les requêtes de streaming de bout en bout.
Détection de caractéristiques
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 {
// …
}
Si vous êtes curieux, voici comment fonctionne la détection des fonctionnalités :
Si le navigateur n'est pas compatible avec un type body
particulier, il appelle toString()
sur l'objet et utilise le résultat comme corps.
Ainsi, si le navigateur ne prend pas en charge les flux de requêtes, le corps de la requête devient la chaîne "[object ReadableStream]"
.
Lorsqu'une chaîne est utilisée comme corps, l'en-tête Content-Type
est défini sur text/plain;charset=UTF-8
.
Ainsi, si cet en-tête est défini, nous savons que le navigateur ne prend pas en charge les flux dans les objets de requête et nous pouvons quitter le processus plus tôt.
Safari accepte les flux dans les objets de requête, mais n'autorise pas leur utilisation avec fetch
. L'option duplex
est donc testée, ce que Safari ne prend pas en charge actuellement.
Utilisation avec des flux accessibles en écriture
Il est parfois plus facile de travailler avec des flux lorsque vous disposez d'un WritableStream
.
Pour ce faire, vous pouvez utiliser un flux d 'identité, qui est une paire lisible/scriptable qui prend tout ce qui est transmis à son extrémité scriptable et l'envoie à l'extrémité lisible.
Pour en créer un, il vous suffit de créer un TransformStream
sans aucun argument :
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Désormais, tout ce que vous envoyez au flux inscriptible fera partie de la requête. Cela vous permet de composer des flux ensemble. Par exemple, voici un exemple amusant où les données sont extraites d'une URL, compressées et envoyées à une autre URL :
// 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,
});
L'exemple ci-dessus utilise des flux de compression pour compresser des données arbitraires à l'aide de gzip.