Permintaan streaming dengan fetch API

Jake Archibald
Jake Archibald

Mulai Chromium 105, Anda dapat memulai permintaan sebelum seluruh tubuh tersedia dengan menggunakan Streams API.

Anda dapat menggunakannya untuk:

  • Lakukan pemanasan server. Dengan kata lain, Anda bisa memulai permintaan setelah pengguna memfokuskan kolom input teks, dan menghilangkan semua header, lalu menunggu hingga pengguna menekan 'send' sebelum mengirimkan data yang mereka masukkan.
  • Secara bertahap mengirim data yang dihasilkan pada klien, seperti audio, video, atau data input.
  • Membuat kembali soket web melalui HTTP/2 atau HTTP/3.

Namun, karena ini adalah fitur platform web tingkat rendah, jangan dibatasi oleh ide saya. Mungkin Anda bisa memikirkan kasus penggunaan yang jauh lebih menarik untuk meminta streaming.

Demo

Hal ini menunjukkan cara Anda dapat melakukan streaming data dari pengguna ke server, dan mengirim kembali data yang dapat diproses secara real time.

Ya, itu bukan contoh yang paling imajinatif, saya hanya ingin membuatnya sederhana, oke?

Bagaimana cara kerjanya?

Sebelumnya tentang petualangan seru mengambil streaming

Streaming Response telah tersedia di semua browser modern untuk sementara waktu. Mereka memungkinkan Anda mengakses bagian respons yang masuk dari server:

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');

Setiap value adalah Uint8Array byte. Jumlah array yang Anda dapatkan dan ukuran array bergantung pada kecepatan jaringan. Jika koneksi Anda cepat, Anda akan mendapatkan lebih sedikit 'potongan' data yang lebih besar. Jika koneksi Anda lambat, Anda akan mendapatkan lebih banyak potongan yang lebih kecil.

Jika ingin mengonversi byte menjadi teks, Anda dapat menggunakan TextDecoder, atau aliran transformasi yang lebih baru jika browser target mendukungnya:

const response = await fetch(url);
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();

TextDecoderStream adalah aliran data transformasi yang mengambil semua potongan Uint8Array tersebut dan mengonversinya menjadi string.

{i>Stream<i} sangat bagus, karena Anda dapat mulai bertindak berdasarkan data yang telah masuk. Misalnya, jika Anda menerima daftar 100 'hasil', Anda dapat menampilkan hasil pertama segera setelah menerimanya, daripada menunggu semuanya.

Bagaimanapun, itu adalah streaming respons. Hal baru menarik yang ingin saya bahas adalah streaming permintaan.

Isi permintaan streaming

Permintaan dapat memiliki isi:

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

Sebelumnya, Anda harus mempersiapkan seluruh isi aplikasi sebelum dapat memulai permintaan, tetapi sekarang di Chromium 105, Anda dapat menyediakan ReadableStream data Anda sendiri:

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',
});

Kode di atas akan mengirimkan "Ini adalah permintaan lambat" ke server, satu kata dalam satu waktu, dengan jeda satu detik di antara setiap kata.

Setiap bagian isi permintaan harus berupa Uint8Array byte, jadi saya menggunakan pipeThrough(new TextEncoderStream()) untuk melakukan konversi untuk saya.

Batasan

Permintaan streaming adalah kemampuan baru untuk web, sehingga memiliki beberapa batasan:

{i>Half duplex<i}?

Agar aliran data dapat digunakan dalam permintaan, opsi permintaan duplex harus ditetapkan ke 'half'.

Fitur HTTP yang kurang dikenal (meskipun, apakah ini adalah perilaku standar bergantung pada siapa yang Anda tanyai) adalah bahwa Anda dapat mulai menerima respons saat Anda masih mengirimkan permintaan. Namun, cara ini kurang dikenal, sehingga tidak didukung dengan baik oleh server, dan tidak didukung oleh browser apa pun.

Di browser, respons tidak akan tersedia hingga isi permintaan telah dikirim sepenuhnya, meskipun server mengirim respons lebih cepat. Hal ini berlaku untuk semua pengambilan browser.

Pola default ini dikenal sebagai 'half duplex'. Namun, beberapa implementasi, seperti fetch di Deno, ditetapkan secara default ke 'full duplex' untuk pengambilan streaming, yang berarti respons dapat tersedia sebelum permintaan selesai.

Jadi, untuk mengatasi masalah kompatibilitas ini, di browser, duplex: 'half' harus ditentukan pada permintaan yang memiliki isi aliran.

Pada masa mendatang, duplex: 'full' mungkin didukung di browser untuk permintaan streaming dan non-streaming.

Sementara itu, hal terbaik berikutnya dalam komunikasi dupleks adalah membuat satu pengambilan dengan permintaan streaming, lalu membuat pengambilan lain untuk menerima respons streaming. Server akan memerlukan suatu cara untuk mengaitkan kedua permintaan ini, seperti ID di URL. Begitulah cara kerja demo.

Pengalihan terbatas

Beberapa bentuk pengalihan HTTP mengharuskan browser mengirim ulang isi permintaan ke URL lain. Untuk mendukung hal ini, browser harus mem-buffer konten aliran, yang semacamnya mengalahkan titik tersebut, sehingga tidak melakukan hal itu.

Namun, jika permintaan memiliki isi streaming, dan responsnya adalah pengalihan HTTP selain 303, pengambilan akan ditolak dan pengalihan tidak akan diikuti.

Pengalihan 303 diizinkan karena pengalihan secara eksplisit mengubah metode menjadi GET dan menghapus isi permintaan.

Memerlukan CORS dan memicu preflight

Permintaan streaming memiliki isi, tetapi tidak memiliki header Content-Length. Permintaan ini adalah jenis permintaan baru, sehingga CORS diperlukan, dan permintaan ini selalu memicu preflight.

Permintaan no-cors streaming tidak diizinkan.

Tidak berfungsi di HTTP/1.x

Pengambilan akan ditolak jika koneksinya adalah HTTP/1.x.

Hal ini karena, menurut aturan HTTP/1.1, isi permintaan dan respons perlu mengirimkan header Content-Length, sehingga pihak lain mengetahui berapa banyak data yang akan diterimanya, atau mengubah format pesan untuk menggunakan encoding yang dipotong. Dengan potongan encoding, isi dibagi menjadi beberapa bagian, masing-masing dengan panjang kontennya sendiri.

Encoding terpotong cukup umum bila menyangkut respons HTTP/1.1, tetapi sangat jarang terkait permintaan, sehingga memiliki terlalu banyak risiko kompatibilitas.

Potensi masalah

Ini adalah fitur baru yang jarang digunakan di internet saat ini. Berikut adalah beberapa masalah yang harus diwaspadai:

Inkompatibilitas di sisi server

Beberapa server aplikasi tidak mendukung permintaan streaming, dan sebagai gantinya menunggu permintaan penuh diterima sebelum mengizinkan Anda melihat salah satunya, dan hal ini bertentangan dengan intinya. Sebagai gantinya, gunakan server aplikasi yang mendukung streaming, seperti NodeJS atau Deno.

Tapi, Anda belum keluar dari hutan! Server aplikasi, seperti NodeJS, biasanya berada di belakang server lain, sering disebut sebagai "server front-end", yang pada gilirannya mungkin berada di belakang CDN. Jika salah satu dari mereka memutuskan untuk melakukan buffering permintaan sebelum memberikannya ke server berikutnya dalam rantai, Anda akan kehilangan manfaat streaming permintaan.

Inkompatibilitas di luar kendali Anda

Karena fitur ini hanya berfungsi melalui HTTPS, Anda tidak perlu khawatir tentang proxy antara Anda dan pengguna, tetapi pengguna mungkin menjalankan proxy di komputernya. Beberapa perangkat lunak perlindungan internet melakukan hal ini untuk memungkinkannya memantau segala sesuatu yang ada di antara browser dan jaringan, dan mungkin ada kasus di mana perangkat lunak ini mem-buffer isi permintaan.

Jika ingin melindungi dari hal ini, Anda dapat membuat 'pengujian fitur' yang mirip dengan demo di atas, tempat Anda mencoba melakukan streaming beberapa data tanpa menutup streaming. Jika menerima data, server dapat merespons melalui pengambilan yang berbeda. Setelah ini terjadi, Anda tahu bahwa klien mendukung permintaan streaming secara menyeluruh.

Deteksi fitur

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 {
  // …
}

Jika Anda ingin tahu, berikut cara kerja deteksi fitur:

Jika browser tidak mendukung jenis body tertentu, browser akan memanggil toString() pada objek dan menggunakan hasilnya sebagai isi. Jadi, jika browser tidak mendukung streaming permintaan, isi permintaan menjadi string "[object ReadableStream]". Jika digunakan sebagai isi, string dapat dengan mudah menetapkan header Content-Type ke text/plain;charset=UTF-8. Jadi, jika header tersebut disetel, kita tahu bahwa browser tidak mendukung streaming dalam objek permintaan, dan kita dapat keluar lebih awal.

Safari tidak mendukung streaming dalam objek permintaan, tetapi tidak mengizinkannya untuk digunakan dengan fetch sehingga opsi duplex diuji, yang saat ini tidak didukung Safari.

Menggunakan dengan streaming yang dapat ditulis

Terkadang lebih mudah mengolah streaming jika memiliki WritableStream. Anda dapat melakukan ini menggunakan aliran 'identitas', yang merupakan pasangan yang dapat dibaca/ditulis yang mengambil apa pun yang diteruskan ke ujungnya yang dapat ditulis, dan mengirimkannya ke akhir yang dapat dibaca. Anda dapat membuat salah satu dari hal ini dengan membuat TransformStream tanpa argumen apa pun:

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

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

Sekarang, apa pun yang Anda kirim ke aliran yang dapat ditulis itu akan menjadi bagian dari permintaan. Hal ini memungkinkan Anda menyusun streaming bersama. Misalnya, berikut contoh konyol data yang diambil dari satu URL, dikompresi, dan dikirim ke URL lain:

// 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,
});

Contoh di atas menggunakan aliran kompresi untuk mengompresi data arbitrer menggunakan gzip.