Начиная с Chromium 105, вы можете начать запрос до того, как будет доступно все тело, используя Streams API .
Вы можете использовать это для:
- Разогрейте сервер. Другими словами, вы можете начать запрос, как только пользователь наведёт фокус на поле ввода текста, и убрать все заголовки, а затем дождаться, пока пользователь нажмёт кнопку «Отправить», прежде чем отправлять введённые данные.
- Постепенно отправляйте данные, созданные на клиенте, такие как аудио, видео или входные данные.
- Воссоздайте веб-сокеты по протоколу HTTP/2 или HTTP/3.
Но поскольку это низкоуровневая функция веб-платформы, не ограничивайтесь моими идеями. Возможно, вы придумаете куда более интересный вариант использования потоковой передачи запросов.
Ранее о захватывающих приключениях в Fetch-трансляциях
Потоки ответов уже давно доступны во всех современных браузерах. Они позволяют получать доступ к частям ответа по мере их поступления с сервера:
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');
Каждое value
представляет собой массив байтов типа Uint8Array
. Количество и размер получаемых массивов зависят от скорости сети. При быстром подключении вы получите меньше данных, но более крупных фрагментов. При медленном подключении вы получите больше данных, но более мелких фрагментов.
Если вы хотите преобразовать байты в текст, вы можете использовать TextDecoder
или более новый поток преобразования, если ваши целевые браузеры его поддерживают :
const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
TextDecoderStream
— это поток преобразования, который захватывает все эти фрагменты Uint8Array
и преобразует их в строки.
Потоки данных очень удобны, поскольку позволяют обрабатывать данные по мере их поступления. Например, если вы получаете список из 100 «результатов», вы можете отобразить первый результат сразу после его получения, а не ждать, пока появятся все 100.
Так или иначе, это потоки ответов. А вот новая интересная вещь, о которой я хотел рассказать, — это потоки запросов.
Тела потоковых запросов
Запросы могут иметь тела:
await fetch(url, {
method: 'POST',
body: requestBody,
});
Раньше вам нужно было, чтобы все тело было готово к обработке, прежде чем вы могли начать запрос, но теперь в Chromium 105 вы можете предоставить свой собственный ReadableStream
данных:
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',
});
Приведенный выше код отправит на сервер сообщение «Это медленный запрос» по одному слову с паузой в одну секунду между каждым словом.
Каждый фрагмент тела запроса должен представлять собой массив байтов Uint8Array
, поэтому я использую pipeThrough(new TextEncoderStream())
для выполнения преобразования.
Ограничения
Потоковые запросы — это новая возможность для Интернета, поэтому они имеют несколько ограничений:
Полудуплекс?
Чтобы разрешить использование потоков в запросе, параметр duplex
запроса необходимо установить на 'half'
.
Малоизвестная функция HTTP (хотя вопрос о том, является ли это стандартным поведением, зависит от того, кого вы спрашиваете) заключается в том, что вы можете начать получать ответ, ещё отправляя запрос. Однако эта функция настолько малоизвестна, что плохо поддерживается серверами и не поддерживается ни одним браузером.
В браузерах ответ не будет доступен до тех пор, пока тело запроса не будет полностью отправлено, даже если сервер отправит ответ раньше. Это справедливо для всех операций загрузки данных браузером.
Этот шаблон по умолчанию известен как «полудуплекс». Однако в некоторых реализациях, например, fetch
Deno , для потоковых выборок по умолчанию используется «полный дуплекс», что означает, что ответ может быть получен до завершения запроса.
Таким образом, чтобы обойти эту проблему совместимости, в браузерах необходимо указывать duplex: 'half'
в запросах, имеющих тело потока.
В будущем duplex: 'full'
может поддерживаться в браузерах для потоковых и непотоковых запросов.
В то же время, наилучший вариант дуплексной связи — сделать одну выборку с потоковым запросом, а затем другую выборку для получения потокового ответа. Серверу потребуется какой-то способ связать эти два запроса, например, идентификатор в URL. Именно так работает демо .
Ограниченные перенаправления
Некоторые виды HTTP-перенаправления требуют, чтобы браузер перенаправил тело запроса на другой URL. Для этого браузеру пришлось бы буферизировать содержимое потока, что, по сути, противоречит сути, поэтому он этого не делает.
Вместо этого, если запрос имеет потоковое тело, а ответ представляет собой HTTP-перенаправление, отличное от 303, выборка будет отклонена и перенаправление не будет выполнено.
Перенаправления 303 разрешены, поскольку они явно меняют метод на GET
и отбрасывают тело запроса.
Требует CORS и запускает предварительную проверку
У потоковых запросов есть тело, но нет заголовка Content-Length
. Это новый тип запросов, поэтому требуется CORS, и такие запросы всегда запускают предварительную проверку.
Запросы на потоковую no-cors
не допускаются.
Не работает на HTTP/1.x
Если соединение HTTP/1.x, выборка будет отклонена.
Это связано с тем, что, согласно правилам HTTP/1.1, тела запросов и ответов должны либо отправлять заголовок Content-Length
, чтобы другая сторона знала, сколько данных она получит, либо изменять формат сообщения, используя фрагментарное кодирование . При фрагментарном кодировании тело разделяется на части, каждая из которых имеет свою длину содержимого.
Кодирование по частям довольно распространено, когда дело касается ответов HTTP/1.1, но очень редко, когда дело касается запросов , поэтому оно представляет слишком большой риск совместимости.
Потенциальные проблемы
Это новая функция, которая пока ещё недостаточно используется в интернете. Вот несколько моментов, на которые стоит обратить внимание:
Несовместимость на стороне сервера
Некоторые серверы приложений не поддерживают потоковые запросы и вместо этого ждут получения полного запроса, прежде чем вы сможете его увидеть, что несколько противоречит сути. Вместо этого используйте сервер приложений с поддержкой потоковой передачи, например, NodeJS или Deno .
Но вы ещё не вышли из сложной ситуации! Сервер приложений, такой как NodeJS, обычно находится за другим сервером, часто называемым «сервером фронтенда», который, в свою очередь, может быть расположен за CDN. Если какой-либо из них решит буферизировать запрос перед передачей его следующему серверу в цепочке, вы потеряете преимущество потоковой передачи запросов.
Несовместимость вне вашего контроля
Поскольку эта функция работает только по протоколу HTTPS, вам не нужно беспокоиться о прокси-серверах между вами и пользователем, но пользователь может использовать прокси-сервер на своём компьютере. Некоторые программы интернет-защиты используют это, чтобы отслеживать всё, что происходит между браузером и сетью, и в некоторых случаях могут буферизировать тело запроса.
Если вы хотите защититься от этого, можно создать «тест функции», аналогичный демонстрационной версии выше , где вы пытаетесь передать поток данных, не закрывая поток. Если сервер получает данные, он может ответить другим методом выборки. Как только это произойдет, вы будете знать, что клиент поддерживает сквозные потоковые запросы.
Обнаружение особенностей
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 {
// …
}
Если вам интересно, вот как работает обнаружение объектов:
Если браузер не поддерживает определённый тип body
, он вызывает toString()
объекта и использует результат в качестве тела запроса. Таким образом, если браузер не поддерживает потоки запроса, тело запроса становится строкой "[object ReadableStream]"
. При использовании строки в качестве тела запроса заголовок Content-Type
устанавливается в значение text/plain;charset=UTF-8
. Таким образом, если этот заголовок установлен, мы знаем, что браузер не поддерживает потоки в объектах запроса, и можем завершить работу досрочно.
Safari поддерживает потоки в объектах запроса, но не позволяет использовать их с fetch
, поэтому тестируется duplex
вариант, который Safari в настоящее время не поддерживает.
Использование с записываемыми потоками
Иногда работать с потоками проще, когда есть WritableStream
. Это можно сделать, используя поток «identity» — пару «чтение/запись», которая принимает всё, что передаётся на его записываемый конец, и отправляет на считываемый конец. Создать такой поток можно, создав TransformStream
без аргументов:
const {readable, writable} = new TransformStream();
const responsePromise = fetch(url, {
method: 'POST',
body: readable,
});
Теперь всё, что вы отправляете в записываемый поток, будет частью запроса. Это позволяет объединять потоки. Например, вот забавный пример, где данные извлекаются с одного URL, сжимаются и отправляются на другой 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,
});
В приведенном выше примере используются потоки сжатия для сжатия произвольных данных с помощью gzip.