A partir de Chromium 105, puedes iniciar una solicitud antes de tener todo el cuerpo disponible con la API de Streams.
Puedes usar esta herramienta para hacer lo siguiente:
- Calienta el servidor. En otras palabras, podrías iniciar la solicitud una vez que el usuario enfoque un campo de entrada de texto, quitar todos los encabezados y, luego, esperar a que el usuario presione "Enviar" antes de enviar los datos que ingresó.
- Envía gradualmente los datos generados en el cliente, como audio, video o datos de entrada.
- Vuelve a crear sockets web a través de HTTP/2 o HTTP/3.
Sin embargo, como se trata de una función de plataforma web de bajo nivel, no te limites a mis ideas. Quizás se te ocurra un caso de uso mucho más interesante para la transmisión de solicitudes.
Anteriormente, en las emocionantes aventuras de las transmisiones de recuperación
Los flujos de respuesta están disponibles en todos los navegadores modernos desde hace un tiempo. Te permiten acceder a partes de una respuesta a medida que llegan del servidor:
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');
Cada value
es un Uint8Array
de bytes.
La cantidad y el tamaño de los arrays que obtienes dependen de la velocidad de la red.
Si tienes una conexión rápida, recibirás menos "fragmentos" de datos, pero más grandes.
Si tienes una conexión lenta, recibirás más fragmentos más pequeños.
Si deseas convertir los bytes en texto, puedes usar TextDecoder
o el flujo de transformación más reciente si tus navegadores objetivo lo admiten:
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
es un flujo de transformación que toma todos esos fragmentos Uint8Array
y los convierte en cadenas.
Los flujos son excelentes, ya que puedes comenzar a actuar sobre los datos a medida que llegan. Por ejemplo, si recibes una lista de 100 "resultados", puedes mostrar el primer resultado en cuanto lo recibas, en lugar de esperar a que se muestren los 100.
De todos modos, eso son los flujos de respuesta. Lo nuevo y emocionante de lo que quería hablar son los flujos de solicitud.
Cuerpos de solicitudes de transmisión
Las solicitudes pueden tener cuerpos:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Anteriormente, necesitabas tener todo el cuerpo listo antes de poder iniciar la solicitud, pero ahora, en Chromium 105, puedes proporcionar tu propio ReadableStream
de datos:
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',
});
El comando anterior enviará "Esta es una solicitud lenta" al servidor, una palabra a la vez, con una pausa de un segundo entre cada palabra.
Cada fragmento de un cuerpo de solicitud debe ser un Uint8Array
de bytes, por lo que uso pipeThrough(new TextEncoderStream())
para que realice la conversión por mí.
Restricciones
Las solicitudes de transmisión son una nueva potencia para la Web, por lo que tienen algunas restricciones:
¿Es half duplex?
Para permitir que se usen transmisiones en una solicitud, la opción de solicitud duplex
debe establecerse en 'half'
.
Una función poco conocida de HTTP (aunque, si este es un comportamiento estándar depende de a quién le preguntes) es que puedes comenzar a recibir la respuesta mientras sigues enviando la solicitud. Sin embargo, es tan poco conocido que los servidores no lo admiten bien y ningún navegador lo admite.
En los navegadores, la respuesta nunca está disponible hasta que se envía por completo el cuerpo de la solicitud, incluso si el servidor envía una respuesta antes. Esto se aplica a todas las recuperaciones del navegador.
Este patrón predeterminado se conoce como "semidúplex".
Sin embargo, algunas implementaciones, como fetch
en Deno, se establecieron de forma predeterminada en "dúplex completo" para las recuperaciones de transmisión, lo que significa que la respuesta puede estar disponible antes de que se complete la solicitud.
Por lo tanto, para solucionar este problema de compatibilidad, en los navegadores, se debe especificar duplex: 'half'
en las solicitudes que tienen un cuerpo de transmisión.
En el futuro, es posible que duplex: 'full'
sea compatible con los navegadores para solicitudes de transmisión y sin transmisión.
Mientras tanto, la mejor alternativa a la comunicación dúplex es realizar una recuperación con una solicitud de transmisión y, luego, realizar otra recuperación para recibir la respuesta de transmisión. El servidor necesitará alguna forma de asociar estas dos solicitudes, como un ID en la URL. Así funciona la demostración.
Redireccionamientos restringidos
Algunas formas de redireccionamiento HTTP requieren que el navegador vuelva a enviar el cuerpo de la solicitud a otra URL. Para admitir esto, el navegador tendría que almacenar en búfer el contenido de la transmisión, lo que, de alguna manera, anularía el objetivo, por lo que no lo hace.
En cambio, si la solicitud tiene un cuerpo de transmisión y la respuesta es un redireccionamiento HTTP que no sea 303, la recuperación rechazará y no se seguirá el redireccionamiento.
Se permiten los redireccionamientos 303, ya que cambian explícitamente el método a GET
y descartan el cuerpo de la solicitud.
Requiere CORS y activa una comprobación previa
Las solicitudes de transmisión tienen un cuerpo, pero no tienen un encabezado Content-Length
.
Se trata de un nuevo tipo de solicitud, por lo que se requiere CORS, y estas solicitudes siempre activan una verificación previa.
No se permiten solicitudes de no-cors
transmisión.
No funciona en HTTP/1.x
La recuperación se rechazará si la conexión es HTTP/1.x.
Esto se debe a que, según las reglas de HTTP/1.1, los cuerpos de solicitud y respuesta deben enviar un encabezado Content-Length
para que el otro lado sepa cuántos datos recibirá, o bien cambiar el formato del mensaje para usar la codificación en fragmentos. Con la codificación en fragmentos, el cuerpo se divide en partes, cada una con su propia longitud de contenido.
La codificación en fragmentos es bastante común cuando se trata de respuestas de HTTP/1.1, pero muy rara cuando se trata de solicitudes, por lo que representa un riesgo de compatibilidad demasiado grande.
Posibles problemas
Esta es una función nueva y una que hoy en día se usa poco en Internet. Estos son algunos problemas que debes tener en cuenta:
Incompatibilidad del lado del servidor
Algunos servidores de aplicaciones no admiten solicitudes de transmisión y, en cambio, esperan a que se reciba la solicitud completa antes de permitirte ver algo de ella, lo que de alguna manera anula el objetivo. En su lugar, usa un servidor de aplicaciones que admita la transmisión, como NodeJS o Deno.
Pero aún no saliste del bosque. El servidor de aplicaciones, como NodeJS, suele estar detrás de otro servidor, a menudo llamado "servidor de frontend", que, a su vez, puede estar detrás de una CDN. Si alguno de ellos decide almacenar en búfer la solicitud antes de enviarla al siguiente servidor de la cadena, perderás el beneficio de la transmisión de solicitudes.
Incompatibilidad fuera de tu control
Dado que esta función solo funciona a través de HTTPS, no tienes que preocuparte por los proxies entre tú y el usuario, pero es posible que el usuario esté ejecutando un proxy en su máquina. Algunos software de protección de Internet hacen esto para poder supervisar todo lo que pasa entre el navegador y la red, y puede haber casos en los que este software almacene en búfer los cuerpos de las solicitudes.
Si quieres protegerte contra esto, puedes crear una "prueba de funciones" similar a la demostración anterior, en la que intentes transmitir algunos datos sin cerrar la transmisión. Si el servidor recibe los datos, puede responder a través de otra recuperación. Una vez que esto suceda, sabrás que el cliente admite solicitudes de transmisión de extremo a extremo.
Detección de características
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 tienes curiosidad, aquí te explicamos cómo funciona la detección de funciones:
Si el navegador no admite un tipo de body
en particular, llama a toString()
en el objeto y usa el resultado como cuerpo.
Por lo tanto, si el navegador no admite transmisiones de solicitudes, el cuerpo de la solicitud se convierte en la cadena "[object ReadableStream]"
.
Cuando se usa una cadena como cuerpo, se configura el encabezado Content-Type
como text/plain;charset=UTF-8
.
Por lo tanto, si se configura ese encabezado, sabemos que el navegador no admite transmisiones en objetos de solicitud y podemos salir antes.
Safari admite transmisiones en objetos de solicitud, pero no permite que se usen con fetch
, por lo que se prueba la opción duplex
, que Safari no admite actualmente.
Uso con transmisiones de escritura
A veces, es más fácil trabajar con transmisiones cuando tienes un WritableStream
.
Puedes hacerlo con un flujo de "identidad", que es un par de lectura y escritura que toma todo lo que se pasa a su extremo de escritura y lo envía al extremo de lectura.
Puedes crear uno de estos objetos creando un TransformStream
sin argumentos:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Ahora, todo lo que envíes al flujo de escritura formará parte de la solicitud. Esto te permite componer transmisiones juntas. Por ejemplo, aquí tienes un ejemplo simple en el que se recuperan datos de una URL, se comprimen y se envían a otra 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,
});
En el ejemplo anterior, se usan flujos de compresión para comprimir datos arbitrarios con gzip.