Permintaan streaming dengan fetch API

Jake Archibald
Jake Archibald

Dari Chromium 105, Anda dapat memulai permintaan sebelum seluruh isi tersedia menggunakan Streams API.

Anda dapat menggunakan ini untuk:

  • Lakukan pemanasan server. Dengan kata lain, Anda bisa memulai permintaan setelah pengguna memfokuskan bidang {i>input <i}teks, dan mengeluarkan semua {i>header<i}, lalu menunggu hingga pengguna menekan 'kirim' sebelum mengirim data yang mereka masukkan.
  • Secara bertahap mengirim data yang dihasilkan di klien, seperti data audio, video, atau input.
  • Membuat ulang 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 streaming permintaan.

Demo

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

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

Ngomong-ngomong, bagaimana cara kerjanya?

Sebelumnya pada petualangan seru streaming pengambilan

Untuk saat ini, aliran Response telah tersedia di semua browser modern. Mereka memungkinkan Anda mengakses bagian-bagian dari sebuah respons ketika mereka 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 dan ukuran array bergantung pada kecepatan jaringan. Jika menggunakan koneksi internet yang cepat, Anda akan mendapatkan lebih sedikit 'bagian' yang lebih besar data. Jika koneksi Anda lambat, Anda akan mendapatkan potongan yang lebih kecil dan lebih banyak.

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 transformasi yang mengambil semua potongan Uint8Array tersebut dan mengonversinya menjadi string.

Aliran data itu bagus, karena Anda dapat mulai bertindak berdasarkan data yang baru tersedia. Misalnya, jika Anda menerima daftar 100 'hasil', Anda dapat menampilkan hasil pertama segera setelah Anda menerimanya, bukan menunggu 100 seluruhnya.

Itu adalah aliran respons. Hal baru yang menarik yang ingin saya bicarakan adalah aliran permintaan.

Isi permintaan streaming

Permintaan dapat memiliki isi:

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

Sebelumnya, seluruh isi harus siap digunakan sebelum dapat memulai permintaan, tetapi sekarang di Chromium 105, Anda dapat memberikan data ReadableStream 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',
});

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

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

Pembatasan

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

{i>Half duplex<i}?

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

Fitur HTTP yang kurang dikenal (meskipun demikian, apakah perilaku standar bergantung pada orang yang Anda tanyai) adalah Anda dapat mulai menerima respons saat Anda masih mengirim permintaan. Tetapi, teknologi ini kurang banyak diketahui, tidak didukung dengan baik oleh server, dan tidak didukung oleh browser apa pun.

Di browser, respons tidak akan tersedia hingga isi permintaan dikirim sepenuhnya, meskipun server mengirimkan 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, artinya respons dapat tersedia sebelum permintaan selesai.

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

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

Sementara itu, hal terbaik berikutnya dalam komunikasi dupleks adalah melakukan satu pengambilan dengan permintaan streaming, lalu melakukan pengambilan lainnya 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 streaming, yang semacam menggagalkan titik, sehingga browser tidak melakukan hal tersebut.

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

Pengalihan 303 diizinkan, karena 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. Itu adalah jenis permintaan baru, jadi CORS diperlukan, dan permintaan ini selalu memicu preflight.

Permintaan streaming no-cors 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 harus mengirimkan header Content-Length sehingga sisi lain mengetahui jumlah data yang akan diterima atau mengubah format pesan untuk menggunakan potongan encoding. Dengan encoding potongan, bagian isi dibagi menjadi beberapa bagian, masing-masing dengan panjang kontennya sendiri.

Bagian encoding cukup umum terjadi pada respons HTTP/1.1, tetapi sangat jarang terkait dengan permintaan, sehingga terlalu banyak risiko kompatibilitas.

Potensi masalah

Ini adalah fitur baru, dan saat ini kurang banyak digunakan di internet. Berikut adalah beberapa masalah yang harus diwaspadai:

Inkompatibilitas di sisi server

Beberapa server aplikasi tidak mendukung permintaan streaming, dan justru menunggu permintaan lengkap diterima sebelum mengizinkan Anda melihat permintaan tersebut, dan ini akan gagal. Sebagai gantinya, gunakan server aplikasi yang mendukung streaming, seperti NodeJS atau Deno.

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

Inkompatibilitas di luar kendali Anda

Karena fitur ini hanya berfungsi melalui HTTPS, Anda tidak perlu mengkhawatirkan proxy antara Anda dan pengguna, tetapi pengguna mungkin menjalankan proxy di komputernya. Beberapa software perlindungan internet melakukan hal ini untuk memungkinkannya memantau semua yang terjadi di antara browser dan jaringan, dan mungkin ada kasus saat software ini mem-buffer badan permintaan.

Jika ingin melindungi dari hal ini, Anda dapat membuat 'pengujian fitur' mirip dengan demo di atas. Di sini, Anda mencoba melakukan streaming beberapa data tanpa menutup streaming. Jika menerima data, server dapat merespons melalui pengambilan yang berbeda. Setelah hal ini terjadi, Anda akan 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 ini 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 stream permintaan, isi permintaan menjadi string "[object ReadableStream]". Bila string digunakan sebagai isi, maka dengan mudah menetapkan header Content-Type ke text/plain;charset=UTF-8. Jadi, jika header tersebut ditetapkan, kita tahu bahwa browser tidak mendukung stream dalam objek permintaan, dan kita dapat keluar lebih awal.

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

Menggunakan dengan streaming yang dapat ditulis

Terkadang lebih mudah untuk menggunakan streaming jika Anda memiliki WritableStream. Anda dapat melakukannya menggunakan 'identitas' stream, yakni pasangan yang dapat dibaca/dapat ditulis yang mengambil apa pun yang diteruskan ke ujung yang dapat ditulis, dan mengirimkannya ke akhir yang dapat dibaca. Anda dapat membuat salah satunya 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 {i>writable stream<i} akan menjadi bagian dari permintaan. Dengan begitu, Anda dapat membuat streaming bersama-sama. Misalnya, berikut ini contoh konyol di mana data 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.