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, puedes iniciar la solicitud una vez que el usuario enfoque un campo de entrada de texto y quitar todos los encabezados, y luego esperar hasta 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 por mis ideas. Quizás puedas pensar en un caso de uso mucho más interesante para la transmisión de solicitudes.
Demostración
Esto muestra cómo puedes transmitir datos del usuario al servidor y enviar datos que se pueden procesar en tiempo real.
Sí, no es el ejemplo más imaginativo, solo quería mantener la sencillez, ¿de acuerdo?
De cualquier manera, ¿cómo funciona esto?
Anteriormente, en las emocionantes aventuras de las transmisiones de recuperación
Las transmisiones de Response ya están disponibles en todos los navegadores modernos desde hace 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 de arrays que obtienes y su tamaño 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, obtendrá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 de destino 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.
Las transmisiones son excelentes, ya que puedes comenzar a actuar en los datos a medida que llegan. Por ejemplo, si recibes una lista de 100 "resultados", puedes mostrar el primer resultado en cuanto lo recibes, en lugar de esperar a los 100.
De cualquier manera, eso son flujos de respuesta. Lo nuevo y emocionante de lo que quería hablar son los flujos de solicitudes.
Cuerpos de solicitud de transmisión
Las solicitudes pueden tener los siguientes cuerpos:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Anteriormente, necesitabas que todo el cuerpo estuviera listo para comenzar 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',
});
Lo anterior enviará "This is a slow request" al servidor, una palabra a la vez, con una pausa de un segundo entre cada palabra.
Cada fragmento del cuerpo de una solicitud debe ser un Uint8Array
de bytes, por lo que uso pipeThrough(new TextEncoderStream())
para hacer la conversión por mí.
Restricciones
Las solicitudes de transmisión son una nueva función para la Web, por lo que tienen algunas restricciones:
¿Dúplex medio?
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 aún envías 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 estará disponible hasta que se haya enviado 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 "dúplex medio".
Sin embargo, algunas implementaciones, como fetch
en Deno, usan de forma predeterminada el “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, 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 demo.
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, en cierto modo, anularía el propósito, 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 es 303, se rechazará la recuperación y no se seguirá el redireccionamiento.
Se permiten redireccionamientos 303, ya que cambian el método a GET
de forma explícita 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
.
Es un tipo de solicitud nuevo, por lo que se requiere CORS, y estas solicitudes siempre activan una verificación previa.
No se permiten solicitudes de no-cors
de 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 la otra parte sepa cuántos datos recibirá o cambiar el formato del mensaje para usar la codificación por fragmentos. Con la codificación por fragmentos, el cuerpo se divide en partes, cada una con su propia longitud de contenido.
La codificación por fragmentos es bastante común en el caso de las respuestas HTTP/1.1, pero es muy rara en el caso de las solicitudes, por lo que representa un riesgo de compatibilidad demasiado alto.
Posibles problemas
Esta es una función nueva que aún no se usa mucho en Internet. Estos son algunos problemas que debes tener en cuenta:
Incompatibilidad del servidor
Algunos servidores de aplicaciones no admiten solicitudes de transmisión y, en su lugar, esperan a que se reciba la solicitud completa antes de permitirte verla, lo que hace que pierdan el sentido. En su lugar, usa un servidor de apps que admita la transmisión, como NodeJS o Deno.
Pero aún no estás fuera de peligro. 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 entregarla 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 debes preocuparte por los proxies entre tú y el usuario, pero es posible que el usuario ejecute 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 almacenen en búfer los cuerpos de solicitud.
Si quieres protegerte contra esto, puedes crear una "prueba de funciones" similar a la demo anterior, en la que intentas transmitir algunos datos sin cerrar la transmisión. Si el servidor recibe los datos, puede responder a través de una recuperación diferente. Una vez que esto suceda, sabrás que el cliente admite solicitudes de transmisión de extremo a extremo.
Detección de atributos
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 mostramos cómo funciona la detección de componentes:
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 flujos de solicitudes, el cuerpo de la solicitud se convierte en la cadena "[object ReadableStream]"
.
Cuando se usa una cadena como cuerpo, se configura convenientemente el encabezado Content-Type
como text/plain;charset=UTF-8
.
Por lo tanto, si se establece ese encabezado, sabemos que el navegador no admite transmisiones en objetos de solicitud y podemos salir antes.
Safari sí 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.
Cómo usar con flujos escribibles
A veces, es más fácil trabajar con flujos 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.
Para crear uno de estos, crea 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 será parte de la solicitud. Esto te permite componer transmisiones juntas. Por ejemplo, este es un ejemplo sencillo 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.