A partir do Chromium 105, é possível iniciar uma solicitação antes de ter todo o corpo disponível usando a API Streams.
Você pode usar esse recurso para:
- Aqueça o servidor. Em outras palavras, você pode iniciar a solicitação assim que o usuário focar um campo de entrada de texto, remover todos os cabeçalhos e esperar até que o usuário pressione "Enviar" antes de enviar os dados inseridos.
- Envie gradualmente os dados gerados no cliente, como áudio, vídeo ou dados de entrada.
- Recrie websockets por HTTP/2 ou HTTP/3.
Mas como esse é um recurso de plataforma da Web de baixo nível, não se limite às minhas ideias. Talvez você consiga pensar em um caso de uso muito mais interessante para o streaming de solicitações.
Anteriormente nas emocionantes aventuras de fluxos de busca
Os fluxos de resposta estão disponíveis em todos os navegadores modernos há algum tempo. Eles permitem acessar partes de uma resposta à medida que chegam do 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
é um Uint8Array
de bytes.
O número e o tamanho dos arrays dependem da velocidade da rede.
Se você estiver em uma conexão rápida, vai receber menos "pedaços" de dados, mas eles serão maiores.
Se você estiver em uma conexão lenta, vai receber mais partes menores.
Se você quiser converter os bytes em texto, use TextDecoder
ou o fluxo de transformação mais recente, se os navegadores de destino forem compatíveis:
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
é um stream de transformação que pega todos esses blocos Uint8Array
e os converte em strings.
Os fluxos são ótimos, porque você pode começar a agir com base nos dados assim que eles chegam. Por exemplo, se você estiver recebendo uma lista de 100 "resultados", poderá mostrar o primeiro assim que o receber, em vez de esperar pelos 100.
De qualquer forma, essas são as transmissões de resposta. O novo recurso interessante que eu queria falar são as transmissões de solicitação.
Corpos de solicitações de streaming
As solicitações podem ter corpos:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Antes, era necessário ter todo o corpo pronto para iniciar a solicitação, mas agora, no Chromium 105, você pode fornecer seu próprio ReadableStream
de dados:
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',
});
O comando acima vai enviar "This is a slow request" (Esta é uma solicitação lenta) para o servidor, uma palavra por vez, com uma pausa de um segundo entre cada palavra.
Cada parte de um corpo de solicitação precisa ser um Uint8Array
de bytes. Por isso, estou usando pipeThrough(new TextEncoderStream())
para fazer a conversão.
Restrições
As solicitações de streaming são um novo recurso para a Web e, por isso, têm algumas restrições:
Half-duplex?
Para permitir que streams sejam usados em uma solicitação, a opção de solicitação duplex
precisa ser definida como 'half'
.
Um recurso pouco conhecido do HTTP (embora o comportamento padrão dependa de quem você perguntar) é que você pode começar a receber a resposta enquanto ainda está enviando a solicitação. No entanto, ele é tão pouco conhecido que não tem suporte adequado dos servidores e não é compatível com nenhum navegador.
Nos navegadores, a resposta nunca fica disponível até que o corpo da solicitação seja totalmente enviado, mesmo que o servidor envie uma resposta antes. Isso é válido para todas as buscas do navegador.
Esse padrão padrão é conhecido como "half duplex".
No entanto, algumas implementações, como fetch
no Deno, usavam "full duplex" como padrão para buscas de streaming, o que significa que a resposta pode ficar disponível antes da conclusão da solicitação.
Para contornar esse problema de compatibilidade, nos navegadores, duplex: 'half'
precisa ser especificado em solicitações que têm um corpo de stream.
No futuro, o duplex: 'full'
poderá ser compatível com navegadores para solicitações de streaming e sem streaming.
Enquanto isso, a melhor alternativa para a comunicação duplex é fazer uma busca com uma solicitação de streaming e, em seguida, fazer outra busca para receber a resposta de streaming. O servidor precisa de uma maneira de associar essas duas solicitações, como um ID no URL. É assim que a demonstração funciona.
Redirecionamentos restritos
Algumas formas de redirecionamento HTTP exigem que o navegador reenvie o corpo da solicitação para outro URL. Para isso, o navegador teria que armazenar em buffer o conteúdo do fluxo, o que meio que prejudica o objetivo. Por isso, ele não faz isso.
Em vez disso, se a solicitação tiver um corpo de streaming e a resposta for um redirecionamento HTTP diferente de 303, a busca será rejeitada e o redirecionamento não será seguido.
Os redirecionamentos 303 são permitidos, já que mudam explicitamente o método para GET
e descartam o corpo da solicitação.
Exige CORS e aciona uma simulação
As solicitações de streaming têm um corpo, mas não têm um cabeçalho Content-Length
.
Esse é um novo tipo de solicitação, então o CORS é necessário, e essas solicitações sempre acionam uma simulação.
Solicitações de streaming no-cors
não são permitidas.
Não funciona no HTTP/1.x
A busca será rejeitada se a conexão for HTTP/1.x.
Isso acontece porque, de acordo com as regras do HTTP/1.1, os corpos de solicitação e resposta precisam enviar um cabeçalho Content-Length
para que o outro lado saiba quantos dados vai receber ou mudar o formato da mensagem para usar a codificação em partes. Com a codificação em partes, o corpo é dividido em partes, cada uma com um comprimento de conteúdo próprio.
A codificação em partes é bastante comum quando se trata de respostas HTTP/1.1, mas muito rara quando se trata de solicitações. Portanto, é um risco de compatibilidade muito grande.
Possíveis problemas
Esse é um recurso novo e pouco usado na Internet atualmente. Confira alguns problemas possíveis:
Incompatibilidade no lado do servidor
Alguns servidores de aplicativos não aceitam solicitações de streaming e esperam que a solicitação completa seja recebida antes de permitir que você veja qualquer parte dela, o que acaba com o objetivo. Em vez disso, use um servidor de apps que ofereça suporte a streaming, como NodeJS ou Deno.
Mas você ainda não está livre! O servidor de aplicativos, como o NodeJS, geralmente fica atrás de outro servidor, muitas vezes chamado de "servidor de front-end", que, por sua vez, pode ficar atrás de uma CDN. Se algum deles decidir armazenar em buffer a solicitação antes de entregá-la ao próximo servidor na cadeia, você perderá o benefício do streaming de solicitações.
Incompatibilidade fora do seu controle
Como esse recurso só funciona por HTTPS, não é preciso se preocupar com proxies entre você e o usuário, mas ele pode estar executando um proxy na máquina dele. Alguns softwares de proteção da Internet fazem isso para monitorar tudo o que passa entre o navegador e a rede, e pode haver casos em que esse software armazena em buffer corpos de solicitação.
Para se proteger contra isso, crie um "teste de recurso" semelhante à demonstração acima, em que você tenta transmitir alguns dados sem fechar o fluxo. Se o servidor receber os dados, ele poderá responder por uma busca diferente. Quando isso acontece, você sabe que o cliente aceita solicitações de streaming de ponta a ponta.
Detecção de recursos
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 {
// …
}
Se você tem interesse, saiba como a detecção de recursos funciona:
Se o navegador não for compatível com um tipo body
específico, ele vai chamar toString()
no objeto e usar o resultado como corpo.
Portanto, se o navegador não oferecer suporte a fluxos de solicitação, o corpo da solicitação se tornará a string "[object ReadableStream]"
.
Quando uma string é usada como um corpo, ela define convenientemente o cabeçalho Content-Type
como text/plain;charset=UTF-8
.
Assim, se esse cabeçalho estiver definido, saberemos que o navegador não oferece suporte a fluxos em objetos de solicitação e podemos sair mais cedo.
O Safari é compatível com streams em objetos de solicitação, mas não permite que eles sejam usados com fetch
. Por isso, a opção duplex
é testada, mas o Safari não é compatível com ela no momento.
Como usar com streams graváveis
Às vezes, é mais fácil trabalhar com streams quando você tem um WritableStream
.
Para isso, use um fluxo de "identidade", que é um par legível/gravável que recebe tudo o que é transmitido para a extremidade gravável e envia para a extremidade legível.
Para criar um deles, crie um TransformStream
sem argumentos:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Agora, tudo o que você enviar para o stream gravável fará parte da solicitação. Isso permite compor streams juntos. Por exemplo, veja um exemplo simples em que os dados são buscados de um URL, compactados e enviados para outro:
// 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,
});
O exemplo acima usa fluxos de compactação para compactar dados arbitrários usando gzip.