Getirme API'si ile akış istekleri

Jake Archibald
Jake Archibald

Chromium 105'ten itibaren Streams API'yi kullanarak tüm gövde kullanılabilir olmadan önce istek başlatabilirsiniz.

Bu özelliği kullanarak şunları yapabilirsiniz:

  • Sunucuyu ısıtın. Diğer bir deyişle, kullanıcı bir metin girişi alanına odaklandığında isteği başlatabilir ve tüm başlıkları kaldırabilirsiniz. Ardından, kullanıcının girdiği verileri göndermeden önce "Gönder" düğmesine basmasını bekleyebilirsiniz.
  • İstemcide oluşturulan verileri (ör. ses, video veya giriş verileri) kademeli olarak gönderin.
  • HTTP/2 veya HTTP/3 üzerinden web soketlerini yeniden oluşturun.

Ancak bu, düşük düzeyli bir web platformu özelliği olduğundan benim fikirlerimle sınırlı kalmayın. Belki de istek akışı için çok daha heyecan verici bir kullanım alanı düşünebilirsiniz.

Daha önce, getirme akışlarının heyecan verici maceralarında

Yanıt akışları bir süredir tüm modern tarayıcılarda kullanılabilmektedir. Bu yöntemler, sunucudan gelen yanıtların bir kısmına ulaşmanızı sağlar:

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

Her value, Uint8Array baytlık bir birimdir. Elde edeceğiniz dizilerin sayısı ve boyutu, ağın hızına bağlıdır. Hızlı bir bağlantı kullanıyorsanız daha az sayıda ve daha büyük veri "parçaları" alırsınız. Yavaş bir bağlantı kullanıyorsanız daha fazla ve daha küçük parçalar alırsınız.

Baytları metne dönüştürmek istiyorsanız TextDecoder veya hedef tarayıcılarınız destekliyorsa daha yeni olan transform stream'i kullanabilirsiniz:

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

TextDecoderStream, tüm bu Uint8Array parçalarını alıp dizelere dönüştüren bir dönüştürme akışıdır.

Veriler geldikçe işlem yapmaya başlayabileceğiniz için akışlar çok kullanışlıdır. Örneğin, 100 "sonuç" listesi alıyorsanız 100 sonucun tamamını beklemek yerine ilk sonucu alır almaz görüntüleyebilirsiniz.

Her neyse, yanıt akışları böyle. Bahsetmek istediğim heyecan verici yeni özellik ise istek akışları.

Akış isteği içerikleri

İstekler aşağıdaki gövdelere sahip olabilir:

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

Daha önce, isteği başlatabilmek için tüm gövdenin hazır olması gerekiyordu. Ancak artık Chromium 105'te kendi ReadableStream verilerinizi sağlayabilirsiniz:

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

Yukarıdaki kod, "Bu yavaş bir istek" ifadesini sunucuya kelime kelime ve her kelime arasında bir saniye duraklayarak gönderir.

İstek gövdesinin her bir parçası Uint8Array bayt olmalıdır. Bu nedenle, dönüşümü benim için yapmak üzere pipeThrough(new TextEncoderStream()) kullanıyorum.

Kısıtlamalar

Yayın isteği, web için yeni bir güçtür. Bu nedenle, birkaç kısıtlamayla birlikte gelir:

Yarım çift yönlü mü?

Akışların bir istekte kullanılmasına izin vermek için duplex istek seçeneğinin 'half' olarak ayarlanması gerekir.

HTTP'nin az bilinen bir özelliği (standart davranış olup olmadığı sorunuza bağlıdır) isteği göndermeye devam ederken yanıtı almaya başlayabilmenizdir. Ancak bu yöntem o kadar az bilinir ki sunucular tarafından iyi desteklenmez ve hiçbir tarayıcı tarafından desteklenmez.

Tarayıcılarda, sunucu daha erken bir yanıt gönderse bile istek gövdesi tamamen gönderilene kadar yanıt hiçbir zaman kullanılamaz. Bu durum tüm tarayıcı getirme işlemleri için geçerlidir.

Bu varsayılan kalıp "yarı çift yönlü" olarak bilinir. Ancak Deno'daki fetch gibi bazı uygulamalarda, akış getirme işlemleri için varsayılan olarak "tam çift yönlü" kullanılır. Bu da yanıtın, istek tamamlanmadan önce kullanılabilir hale gelebileceği anlamına gelir.

Bu nedenle, bu uyumluluk sorununu gidermek için tarayıcılarda akış gövdesi olan isteklerde duplex: 'half' belirtilmesi gerekir.

Gelecekte duplex: 'full', yayın ve yayın dışı istekler için tarayıcılarda desteklenebilir.

Bu arada, çift yönlü iletişime en yakın alternatif, akış isteğiyle bir getirme işlemi yapmak, ardından akış yanıtını almak için başka bir getirme işlemi yapmaktır. Sunucunun, bu iki isteği ilişkilendirmek için URL'deki bir kimlik gibi bir yönteme ihtiyacı vardır. Demo bu şekilde çalışır.

Kısıtlanmış yönlendirmeler

Bazı HTTP yönlendirme biçimlerinde tarayıcının isteğin gövdesini başka bir URL'ye yeniden göndermesi gerekir. Bunu desteklemek için tarayıcının akışın içeriğini arabelleğe alması gerekir. Bu da bir bakıma amacını boşa çıkarır. Bu nedenle tarayıcı bunu yapmaz.

Bunun yerine, istekte akış gövdesi varsa ve yanıt 303 dışında bir HTTP yönlendirmesi ise getirme işlemi reddedilir ve yönlendirme izlenmez.

Yöntemi açıkça GET olarak değiştirdikleri ve istek gövdesini sildikleri için 303 yönlendirmelerine izin verilir.

CORS gerektirir ve ön uç isteğini tetikler

Yayın isteklerinin gövdesi vardır ancak Content-Length üstbilgisi yoktur. Bu yeni bir istek türü olduğundan CORS gereklidir ve bu istekler her zaman ön kontrolü tetikler.

Yayın no-cors isteklerine izin verilmez.

HTTP/1.x'te çalışmaz

Bağlantı HTTP/1.x ise getirme işlemi reddedilir.

Bunun nedeni, HTTP/1.1 kurallarına göre istek ve yanıt gövdelerinin, diğer tarafın ne kadar veri alacağını bilmesi için bir Content-Length üstbilgisi göndermesi veya mesajın biçimini değiştirerek parçalı kodlama kullanması gerekmesidir. Parçalı kodlamada gövde, her biri kendi içerik uzunluğuna sahip olan parçalara bölünür.

Parçalı kodlama, HTTP/1.1 yanıtları için oldukça yaygın olsa da istekler için çok nadir olduğundan uyumluluk açısından çok büyük bir risk oluşturur.

Olası sorunlar

Bu yeni bir özellik olup internette günümüzde yeterince kullanılmamaktadır. Dikkat etmeniz gereken bazı sorunlar:

Sunucu tarafında uyumsuzluk

Bazı uygulama sunucuları, akış isteklerini desteklemez ve isteğin tamamı alınana kadar hiçbir bölümünü görmenize izin vermez. Bu durum, akışın amacını bozar. Bunun yerine, NodeJS veya Deno gibi akışı destekleyen bir uygulama sunucusu kullanın.

Ancak henüz tehlike geçmiş değil. NodeJS gibi uygulama sunucuları genellikle başka bir sunucunun (çoğu zaman "ön uç sunucusu" olarak adlandırılır) arkasında yer alır. Bu sunucu da CDN'nin arkasında yer alabilir. Bu sunuculardan herhangi biri, isteği zincirdeki bir sonraki sunucuya vermeden önce arabelleğe almaya karar verirse istek akışının avantajını kaybedersiniz.

Kontrolünüz dışındaki uyumsuzluklar

Bu özellik yalnızca HTTPS üzerinden çalıştığı için sizinle kullanıcı arasındaki proxy'ler konusunda endişelenmenize gerek yoktur ancak kullanıcı, makinesinde bir proxy çalıştırıyor olabilir. Bazı internet koruma yazılımları, tarayıcı ile ağ arasında geçen her şeyi izleyebilmek için bunu yapar ve bu yazılımın istek gövdelerini arabelleğe aldığı durumlar olabilir.

Bu duruma karşı korunmak istiyorsanız yukarıdaki demoya benzer bir "özellik testi" oluşturabilirsiniz. Bu testte, akışı kapatmadan bazı verileri yayınlamaya çalışırsınız. Sunucu verileri alırsa farklı bir getirme işlemiyle yanıt verebilir. Bu gerçekleştiğinde istemcinin, uçtan uca yayın isteğini desteklediğini anlarsınız.

Özellik algılama

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

Merak ediyorsanız özellik algılama işleyiş şekli şöyledir:

Tarayıcı belirli bir body türünü desteklemiyorsa nesnede toString() işlevini çağırır ve sonucu gövde olarak kullanır. Bu nedenle, tarayıcı istek akışlarını desteklemiyorsa istek gövdesi "[object ReadableStream]" dizesi olur. Dize gövde olarak kullanıldığında Content-Type başlığı text/plain;charset=UTF-8 olarak ayarlanır. Bu nedenle, bu başlık ayarlanmışsa tarayıcının istek nesnelerinde akışları desteklemediğini biliriz ve erken çıkış yapabiliriz.

Safari, istek nesnelerindeki akışları destekler ancak bunların fetch ile kullanılmasına izin vermez. Bu nedenle, Safari'nin şu anda desteklemediği duplex seçeneği test edilir.

Yazılabilir akışlarla kullanma

Bazen WritableStream olduğunda akışlarla çalışmak daha kolaydır. Bunu, yazılabilir ucuna iletilen her şeyi alıp okunabilir ucuna gönderen, okunabilir/yazılabilir bir çift olan "kimlik" akışını kullanarak yapabilirsiniz. Aşağıdaki yöntemlerden biriyle TransformStream oluşturarak bu türden bir öğe oluşturabilirsiniz:

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

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

Artık yazılabilir akışa gönderdiğiniz her şey isteğin bir parçası olacak. Bu sayede akışları birlikte oluşturabilirsiniz. Örneğin, verilerin bir URL'den getirildiği, sıkıştırıldığı ve başka bir URL'ye gönderildiği komik bir örnek aşağıda verilmiştir:

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

Yukarıdaki örnekte, gzip kullanarak rastgele verileri sıkıştırmak için sıkıştırma akışları kullanılır.