Transmite solicitudes con la API de fetch

A partir de Chromium 105, puedes iniciar una solicitud antes de que todo el contenido esté disponible con la API de Streams.

Podrías usar esto para lo siguiente:

  • Calienta el servidor. En otras palabras, puedes iniciar la solicitud una vez que el usuario enfoque un campo de entrada de texto, quitar todos los encabezados y, luego, esperar hasta que el usuario presione "enviar" antes de enviar los datos que ingresaron.
  • Envía gradualmente datos generados en el cliente, como datos de audio, video o entrada.
  • Vuelve a crear los sockets web a través de HTTP/2 o HTTP/3.

Pero como esta es una función de plataforma web de bajo nivel, no te limites a mis ideas. Tal vez se te ocurra un caso de uso mucho más emocionante para la transmisión de solicitudes.

Demostración

Aquí se muestra cómo puedes transmitir datos del usuario al servidor y devolver datos que se puedan procesar en tiempo real.

Claro, no es el ejemplo más imaginativo, solo quería que fuera simple, ¿de acuerdo?

De todos modos, ¿cómo funciona?

Anteriormente, sobre las emocionantes aventuras de los arroyos recuperados

Hace tiempo que las transmisiones de respuestas están disponibles en todos los navegadores modernos. Te permiten acceder a partes de una respuesta a medida que llegan desde el 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 obtengas y el tamaño de estos dependerán de la velocidad de la red. Si tienes una conexión rápida, recibirás menos “fragmentos” más grandes de los datos. Si tu conexión es lenta, obtendrás más fragmentos más pequeños.

Si deseas convertir los bytes en texto, puedes usar TextDecoder o la transmisión 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 una transmisión de transformación que toma todos esos fragmentos Uint8Array y los convierte en strings.

Las transmisiones son geniales, 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 apenas lo recibas, en lugar de esperar a los 100.

De todos modos, esas son las transmisiones de respuestas, lo nuevo y emocionante de lo que quería hablar es sobre las transmisiones de solicitudes.

Cuerpos de solicitud de transmisión

Las solicitudes pueden tener cuerpos:

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

Anteriormente, se necesitaba que todo el cuerpo esté listo antes de iniciar la solicitud, pero ahora en Chromium 105, puedes proporcionar tus propios 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',
});

La información anterior enviará un mensaje que indica que la solicitud es lenta. al servidor, una palabra a la vez, con una pausa de un segundo entre cada palabra.

Cada parte del cuerpo de una solicitud debe ser un Uint8Array de bytes, por lo que estoy usando pipeThrough(new TextEncoderStream()) para realizar la conversión por mí.

Restricciones

Las solicitudes de transmisión son una nueva potencia 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 característica poco conocida de HTTP (aunque el 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 conocida que no es compatible con los servidores ni con ningún navegador.

En los navegadores, la respuesta nunca está disponible hasta que el cuerpo de la solicitud se haya enviado por completo, 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, se configuraron como "dúplex completo" de forma predeterminada. 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 navegadores, duplex: 'half' necesita que se especificará 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 opción para la comunicación dúplex es hacer 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 manera 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 reenvíe 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 anula el punto, así que no hace eso.

En cambio, si la solicitud tiene un cuerpo de transmisión y la respuesta es un redireccionamiento HTTP distinto del 303, se rechazará la recuperación y no se seguirá el redireccionamiento.

Se permiten los 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. Ese es un nuevo tipo de solicitud, por lo que se requiere CORS, y estas solicitudes siempre activan una verificación previa.

No se permiten las solicitudes de transmisión de no-cors.

No funciona en HTTP/1.x

Se rechazará la recuperación si la conexión es HTTP/1.x.

Esto se debe a que, según las reglas HTTP/1.1, los cuerpos de la solicitud y la respuesta deben enviar un encabezado Content-Length, de modo que la otra parte sepa cuántos datos recibirá, o cambiar el formato del mensaje para usar la codificación fragmentada. Con la codificación fragmentada, el cuerpo se divide en partes, cada una con su propia longitud de contenido.

La codificación fragmentada es bastante común cuando se trata de respuestas HTTP/1.1, pero muy poco frecuente cuando se trata de solicitudes, por lo que representa un riesgo demasiado alto de compatibilidad.

Posibles problemas

Esta es una función nueva que hoy en día se usa poco en Internet. A continuación, se incluyen algunos problemas para tener en cuenta:

Incompatibilidad del servidor

Algunos servidores de apps no admiten solicitudes de transmisión y, en su lugar, esperan a que se reciba la solicitud completa antes de permitirte ver cualquiera de ellas, lo que va en contra de la idea. En su lugar, usa un servidor de apps que admita transmisiones, como NodeJS o Deno.

Pero todavía no te has ido del bosque. El servidor de aplicaciones, como NodeJS, generalmente se ubica detrás de otro servidor, a menudo llamado “servidor frontend”, que, a su vez, puede ubicarse detrás de una CDN. Si alguna de ellas 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 su control

Dado que esta función solo funciona con 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 programas de protección de Internet hacen esto para permitirle 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 solicitud.

Si quieres protegerte contra esto, puedes crear una “prueba de funciones” similar a la demostración anterior, en la que intentas transmitir algunos datos sin cerrarla. Si el servidor recibe los datos, puede responder a través de una recuperación diferente. Una vez que esto sucede, sabes que el cliente admite solicitudes de transmisión de extremo a extremo.

Detección de funciones

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, a continuación te mostramos cómo funciona la detección de características:

Si el navegador no admite un tipo de body específico, 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 string "[object ReadableStream]". Cuando se usa una cadena como cuerpo, establece convenientemente el encabezado Content-Type en text/plain;charset=UTF-8. Entonces, si ese encabezado está configurado, entonces 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 no es compatible con Safari en este momento.

Cómo usar transmisiones con escritura

A veces es más fácil trabajar con transmisiones cuando tienes un WritableStream. Puedes hacer esto con un modelo de “identidad” de transmisión, que es un par de lectura/escritura que toma todo lo que pasa a su extremo de escritura y lo envía al extremo legible. Para crear una de estas opciones, crea un TransformStream sin ningún argumento:

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

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

Ahora, todo lo que envíes a la transmisión que admite escritura forma parte de la solicitud. Esto te permitirá componer transmisiones juntas. A continuación, te mostramos un ejemplo absurdo en el que los datos se recuperan 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 transmisiones de compresión para comprimir datos arbitrarios con gzip.