Tarayıcıda bir sunucuya HTTP isteği göndermenin bugünkü standart yolu fetch() fonksiyonudur. fetch(url) bir Promise döndürür; bu söz, sunucunun durum kodu ve başlıkları geldiği anda bir Response nesnesiyle yerine gelir. Gövdeyi okumak için ayrıca response.json() veya response.text() çağırırsınız. En önemli kural şudur: sunucu 404 ya da 500 döndürse bile söz reddedilmez, bu yüzden her istekte response.ok değerini kendiniz kontrol etmelisiniz.

Bu yazıda GET ve POST isteklerini, JSON ve form verisi göndermeyi, yanıtı doğru biçimde okumayı, hataları ayırt etmeyi ve AbortController ile AbortSignal.timeout() kullanarak istekleri iptal etmeyi adım adım göreceksiniz. Request, Response ve Headers nesnelerinin ayrıntıları için Request, Response ve Headers rehberine, başka bir alan adına istek atarken karşılaşacağınız kurallar için CORS yazısına bakabilirsiniz. Örneklerde async/await kullanılıyor; bu söz dizimi yeniyse önce async/await rehberini okumanızı öneririz.

fetch() nasıl çalışır?

fetch() hem pencere (window) hem de worker bağlamında genel (global) bir fonksiyon olarak bulunur. İlk argüman getirilecek kaynağı tanımlar: bir URL metni, metne dönüştürülebilen bir nesne (örneğin URL) ya da hazır bir Request nesnesi olabilir. İkinci argüman isteğe bağlıdır ve RequestInit adı verilen seçenekler nesnesidir: yöntem, başlıklar, gövde, iptal sinyali ve benzeri ayarlar burada verilir. Göreli bir URL verirseniz sayfada belgenin baseURI değerine, worker içinde ise worker'ın adresine göre çözülür.

İşlem iki aşamada ilerler. Birinci aşamada tarayıcı isteği gönderir ve sunucudan durum satırı ile başlıklar gelir gelmez söz yerine gelir; bu anda gövdenin tamamı henüz inmemiş olabilir. İkinci aşamada siz gövdeyi okuyan bir yöntem çağırırsınız ve bu yöntem de ayrı bir söz döndürür. Bu yüzden tipik bir fetch kodunda iki tane await görürsünüz: biri yanıt nesnesi, biri gövde için.

javascript
async function getData() {
  const url = "https://example.org/products.json";
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }
    const result = await response.json();
    console.log(result);
  } catch (error) {
    console.error(error.message);
  }
}

Bu kalıp MDN'nin temel örneğidir ve neredeyse her projede küçük değişikliklerle tekrar kullanılır: istek atılır, durum kontrol edilir, başarılıysa gövde JSON olarak çözülür, herhangi bir aşamadaki hata tek bir catch bloğunda yakalanır.

fetch() ne zaman reddeder, ne zaman reddetmez?

fetch() ne zaman reddeder, ne zaman reddetmez? Sunucu 200 döndürdü, Sunucu 404, 500 veya 504 döndürdü, Ağ hatası (bağlantı yok, DNS hatası), URL geçersiz veya kullanıcı adı ve...

Yeni başlayanların en sık takıldığı nokta budur. fetch() sözü yalnızca isteğin kendisi başarısız olduğunda reddedilir. Sunucu bir HTTP yanıtı döndürdüyse, bu yanıt bir hata durum kodu taşısa bile söz yerine gelir. Aşağıdaki tablo MDN'deki istisna listesini özetler.

DurumSonuç
Sunucu 200 döndürdüSöz yerine gelir, ok true olur
Sunucu 404, 500 veya 504 döndürdüSöz yine yerine gelir, ok false olur
Ağ hatası (bağlantı yok, DNS hatası)TypeError ile reddedilir
URL geçersiz veya kullanıcı adı ve parola içeriyorTypeError ile reddedilir
Seçeneklerde geçersiz bir değer varTypeError ile reddedilir
İstek bir izin politikasıyla engellendiTypeError ile reddedilir
AbortController ile iptal edildiAbortError adlı DOMException ile reddedilir
AbortSignal.timeout süresi dolduTimeoutError adlı DOMException ile reddedilir

Response.ok özelliği durum kodu 200 ile 299 arasındaysa true, diğer her durumda false olur. Kodun kendisi status, açıklama metni statusText özelliğindedir. Aşağıdaki örnekte ağ kullanmadan, yanıtı doğrudan Response kurucusuyla oluşturuyoruz; böylece aynı özelliklerin nasıl davrandığını görebilirsiniz:

javascript
const yok = new Response("Sayfa bulunamadı", { status: 404, statusText: "Not Found" });
console.log(yok.ok); // false
console.log(yok.status); // 404
console.log(yok.statusText); // Not Found

Durum kodlarının hangi anlama geldiğini merak ediyorsanız HTTP durum kodları yazısında 2xx, 3xx, 4xx ve 5xx gruplarının tamamı açıklanıyor. Ağ hatası ile CORS engelini birbirinden ayırmak ise koddan mümkün değildir; bu konuyu DOMException ve güvenli bağlam hataları yazısında ayrıntılı ele alıyoruz.

Yanıt gövdesini okuma

Response nesnesi gövdeyi farklı biçimlerde okumak için birkaç yöntem sunar. Hepsi asenkrondur ve bir söz döndürür:

YöntemSözün değeriTipik kullanım
json()JSON olarak çözülmüş JavaScript değeriREST API yanıtları
text()Metin (string)HTML, CSV, düz metin
blob()Blob nesnesiGörsel, PDF, indirme
arrayBuffer()ArrayBufferİkili veri işleme
bytes()Uint8ArrayBayt dizisi olarak ikili veri
formData()FormDatamultipart veya urlencoded gövde

Burada adı yanıltıcı olabilecek bir ayrıntı var: json() size JSON metni vermez; JSON metnini çözerek bir JavaScript nesnesi, dizi, sayı veya metin üretir. JSON'un kendisiyle ilgili kurallar için JSON.parse ve JSON.stringify yazısına bakabilirsiniz. Gövde bir akış (stream) olduğu için yalnızca bir kez okunabilir. İkinci kez okumaya çalışırsanız TypeError alırsınız; gövde geçerli JSON değilse json() sözü SyntaxError ile reddedilir:

javascript
const cevap = Response.json({ ad: "Ayşe", puan: 42 });
console.log(cevap.headers.get("content-type")); // application/json
const veri = await cevap.json();
console.log(veri.ad); // Ayşe
console.log(cevap.bodyUsed); // true
try {
  await cevap.json();
} catch (hata) {
  console.log(hata.name); // TypeError
}

const bozuk = new Response("{bozuk json");
try {
  await bozuk.json();
} catch (hata) {
  console.log(hata.name); // SyntaxError
}

Response.json() statik yöntemi, verdiğiniz değeri JSON gövdesi olarak taşıyan ve Content-Type başlığı application/json olan bir yanıt üretir. Gerçek uygulamada bu yanıtı sunucu gönderir; örnekte yalnızca okuma davranışını göstermek için kullandık. Gövdeyi iki kez kullanmanız gerekiyorsa okumadan önce response.clone() çağırmalısınız; ayrıntılar Request ve Response yazısında.

Bir görseli indirip sayfada göstermek istediğinizde blob() ile URL.createObjectURL() birlikte kullanılır. Bu örnek sayfadaki bir <img> elemanına bağlı olduğu için yalnızca tarayıcıda çalışır:

javascript
const image = document.querySelector("img");

async function setImage() {
  try {
    const response = await fetch("flowers.jpg");
    if (!response.ok) {
      throw new Error(`Response status: ${response.status}`);
    }
    const blob = await response.blob();
    image.src = URL.createObjectURL(blob);
  } catch (e) {
    console.error(e);
  }
}

Blob ve dosya nesneleriyle çalışmanın diğer yolları File API ve FileReader yazısında anlatılıyor.

Content-Type başlığını kontrol etmek

Content-Type başlığını kontrol etmek

Bir API bazen JSON yerine bir hata sayfası (HTML) döndürür. Bu durumda json() çağrısı SyntaxError ile patlar ve hata mesajı asıl sorunu gizler. MDN, gövdeyi okumadan önce content-type başlığına bakmayı önerir. Aşağıdaki küçük yardımcı fonksiyon hem durum kodunu hem de içerik türünü denetler; denemek için üç farklı yanıtı yerel olarak oluşturduk:

javascript
async function jsonOku(response) {
  if (!response.ok) {
    throw new Error(`Response status: ${response.status}`);
  }
  const tur = response.headers.get("content-type");
  if (!tur || !tur.includes("application/json")) {
    throw new TypeError("Yanıt JSON değil");
  }
  return response.json();
}

for (const r of [
  Response.json({ urun: "kalem" }),
  new Response("hata", { status: 500 }),
  new Response("<p>merhaba</p>", { headers: { "Content-Type": "text/html" } }),
]) {
  try {
    const sonuc = await jsonOku(r);
    console.log("tamam:", JSON.stringify(sonuc));
  } catch (hata) {
    console.log(hata.name + ": " + hata.message);
  }
}
// tamam: {"urun":"kalem"}
// Error: Response status: 500
// TypeError: Yanıt JSON değil

Gerçek kodda jsonOku(await fetch(url)) biçiminde kullanırsınız. Böylece yanıtı işleyen her yer aynı kontrolden geçer ve hata mesajları sorunun gerçek nedenini gösterir.

GET isteğiyle veri göndermek

fetch() varsayılan olarak GET isteği yapar. GET isteklerinin gövdesi olmaz; veri göndermek istiyorsanız sorgu dizesine (query string) eklersiniz. Değerleri elle birleştirmek yerine URLSearchParams kullanmak, Türkçe karakterleri ve boşlukları doğru biçimde kodlar:

javascript
const params = new URLSearchParams({ q: "çay bardağı", sayfa: 2 });
console.log(`https://example.org/ara?${params}`);
// https://example.org/ara?q=%C3%A7ay+barda%C4%9F%C4%B1&sayfa=2

// Tarayıcıda:
// const response = await fetch(`https://example.org/ara?${params}`);

GET isteğine body eklemeye çalışırsanız istek hiç oluşturulmaz. Request kurucusu ve fetch() aynı parametreleri aldığı için bunu ağ kullanmadan deneyebilirsiniz:

javascript
try {
  new Request("https://example.org/ara", { method: "GET", body: "x" });
} catch (hata) {
  console.log(hata.name); // TypeError
}

POST isteği: JSON, form ve dosya göndermek

Başka bir yöntem kullanmak için method seçeneğini, gönderilecek veri için body seçeneğini verirsiniz. Gövde olarak metin, ArrayBuffer, TypedArray, DataView, Blob, File, URLSearchParams, FormData veya ReadableStream kullanılabilir. Bunların dışındaki nesneler toString() ile metne çevrilir; düz bir nesneyi doğrudan verirseniz sunucuya [object Object] gider. Bu yüzden JSON göndermek için nesneyi önce JSON.stringify() ile metne dönüştürürsünüz:

javascript
const response = await fetch("https://example.org/post", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ username: "example" }),
});

Content-Type başlığını unutmak sık yapılan bir hatadır. Gövde metin olduğunda tarayıcı türü kendisi belirler ve bu tür JSON değildir. Aşağıdaki örnek, aynı gövdeyle başlıklı ve başlıksız iki istek oluşturup tarayıcının atadığı türü gösterir:

javascript
const a = new Request("https://example.org/post", {
  method: "POST",
  body: JSON.stringify({ username: "example" }),
});
console.log(a.headers.get("content-type")); // text/plain;charset=UTF-8

const b = new Request("https://example.org/post", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ username: "example" }),
});
console.log(b.headers.get("content-type")); // application/json

Sunucunuz gövdeyi Content-Type değerine bakarak çözüyorsa, başlık text/plain kaldığında gövdeyi JSON olarak görmeyebilir. Öte yandan application/json türü, başka bir kökene giden isteklerde ön uçuş (preflight) isteğini tetikler. Bu ilişkiyi CORS rehberinde ayrıntılı açıklıyoruz.

Klasik form kodlamasıyla veri göndermek için URLSearchParams gövde olarak verilebilir. Bu durumda tür başlığı otomatik olarak atanır:

javascript
const c = new Request("https://example.org/post", {
  method: "POST",
  body: new URLSearchParams({ username: "example", password: "password" }),
});
console.log(c.headers.get("content-type")); // application/x-www-form-urlencoded;charset=UTF-8
console.log(await c.text()); // username=example&password=password

Dosya yüklemek ya da bir HTML formunun tüm alanlarını göndermek için FormData kullanılır. FormData gövdesinde Content-Type başlığını elle yazmamalısınız; tarayıcı multipart/form-data türünü sınır (boundary) değeriyle birlikte kendisi ekler. Form gönderme olayı, requestSubmit() ve form.elements konuları FormData ile form gönderme yazısında yer alıyor.

İsteği iptal etmek ve zaman aşımı

fetch() kendi başına bir zaman aşımı süresine sahip değildir. İsteği iptal edilebilir yapmak için bir AbortController oluşturur, onun signal özelliğini signal seçeneğine verirsiniz. controller.abort() çağrıldığında söz AbortError ile reddedilir. Yanıt gelmiş ama gövde henüz okunmamışsa, gövdeyi okuyan çağrı da aynı hatayla reddedilir.

javascript
const controller = new AbortController();

document.querySelector("#fetch").addEventListener("click", async () => {
  try {
    const response = await fetch("https://example.org/get", {
      signal: controller.signal,
    });
    console.log(`Response: ${response.status}`);
  } catch (e) {
    console.error(`Error: ${e}`);
  }
});

document.querySelector("#cancel").addEventListener("click", () => {
  controller.abort();
});

Belirli bir süre sonra otomatik iptal için AbortSignal.timeout(ms) kullanılır. Süre dolduğunda söz bu kez TimeoutError adlı bir DOMException ile reddedilir; bu sayede kullanıcının iptal etmesi ile zaman aşımını birbirinden ayırabilirsiniz. Birden fazla sinyali birleştirmek için AbortSignal.any() vardır. Aşağıdaki örnek, ağ kullanmadan sinyallerin hangi nedenle iptal edildiğini gösterir:

javascript
const zaman = AbortSignal.timeout(50);
await new Promise((r) => setTimeout(r, 120));
console.log(zaman.aborted, zaman.reason.name); // true TimeoutError

const kontrol = new AbortController();
kontrol.abort();
console.log(kontrol.signal.aborted, kontrol.signal.reason.name); // true AbortError

const birlesik = AbortSignal.any([new AbortController().signal, AbortSignal.timeout(30)]);
await new Promise((r) => setTimeout(r, 100));
console.log(birlesik.aborted, birlesik.reason.name); // true TimeoutError

Tarayıcıdaki kullanımda hatayı adına göre ayırırsınız:

javascript
try {
  const res = await fetch("video.mp4", { signal: AbortSignal.timeout(5000) });
  const result = await res.blob();
} catch (err) {
  if (err.name === "TimeoutError") {
    console.error("5 saniyede yanıt gelmedi");
  } else if (err.name === "AbortError") {
    console.error("İstek kullanıcı tarafından iptal edildi");
  } else {
    console.error(`Hata: ${err.name}, ${err.message}`);
  }
}

Bir sinyal yalnızca bir kez kullanılabilir: iptal edildikten sonra aynı sinyalle başlatılan her fetch() hemen reddedilir. Her yeni istek için yeni bir AbortController oluşturun. Node.js tarafında zaman aşımı ve dispatcher ayarları farklı konulardır; onlar için Node.js fetch rehberine bakın.

Kimlik bilgileri ve başka kökenlere istek

Fetch açısından kimlik bilgisi (credential) çerezler, TLS istemci sertifikaları ve Authorization başlığıdır. credentials seçeneğinin varsayılan değeri same-origin'dir: kimlik bilgileri yalnızca aynı kökene giden isteklerde gönderilir. omit hiç göndermez, include başka kökenlere de gönderir. include kullandığınızda sunucunun da Access-Control-Allow-Credentials başlığıyla buna izin vermesi ve Access-Control-Allow-Origin değerinde yıldız yerine sizin kökeninizi yazması gerekir.

mode seçeneği varsayılan olarak cors'tur; başka bir kökene giden istekler CORS kurallarına göre değerlendirilir. same-origin başka kökene isteği tamamen yasaklar, no-cors ise okunamayan (opak) bir yanıt döndürür. Bu seçeneklerin tamamı Request, Response ve Headers yazısında, çerezlerin teknik ayrıntıları document.cookie ve Set-Cookie yazısında, kimlik bilgili isteklerin güvenlik boyutu ise CSRF yazısında ele alınıyor.

Büyük yanıtları akış olarak okumak

Yanıt gövdesi aslında bir ReadableStream'dir. text() gibi yöntemler tüm gövdenin inmesini bekler; çok büyük bir metin dosyasında ise parçaları geldikçe işlemek isteyebilirsiniz. MDN'deki örnek, gövdeyi TextDecoderStream üzerinden geçirip asenkron döngüyle okur:

javascript
const response = await fetch("https://www.example.org/a-large-file.txt");
if (!response.ok) {
  throw new Error(`Response status: ${response.status}`);
}
const stream = response.body.pipeThrough(new TextDecoderStream());
for await (const value of stream) {
  console.log(value);
}

Gövdeye bu şekilde doğrudan eriştiğinizde ham baytlarla çalışırsınız ve kod çözme işini sizin yapmanız gerekir. Bir akışa okuyucu bağlandığında akış kilitlenir; içinden veri okunduğunda ise bozulmuş (disturbed) sayılır. İki durumda da başka bir kod aynı gövdeyi okuyamaz.

Sayfa kapanırken istek göndermek

keepalive: true seçeneği, isteği başlatan sayfa kapansa bile tarayıcının isteği yarıda kesmemesini sağlar. Oturum sonunda analitik verisi göndermek için kullanılır ve navigator.sendBeacon()'a göre başka yöntemler kullanabilme, istek özelliklerini özelleştirebilme ve yanıta erişebilme avantajları vardır. MDN'ye göre keepalive isteklerinde gövde boyutu 64 kibibayt ile sınırlıdır. Sayfa yaşam döngüsü olayları için DOMContentLoaded ve sayfa yaşam döngüsü yazısına bakabilirsiniz.

Sık yapılan hatalar

Sık yapılan hatalar: ok kontrolünü atlamak, Gövde sözünü beklememek, Nesneyi JSON.stringify olmadan göndermek, Content-Type başlığını unutmak
  • ok kontrolünü atlamak: 404 veya 500 yanıtı catch bloğuna düşmez; response.ok kontrolü olmadan hata sayfasını veri gibi işlersiniz.
  • Gövde sözünü beklememek: response.json() de bir söz döndürür; await unutulursa değişkende veri yerine bekleyen bir söz olur.
  • Nesneyi JSON.stringify olmadan göndermek: düz nesne toString() ile metne çevrilir ve sunucuya anlamsız bir metin gider.
  • Content-Type başlığını unutmak: JSON metni text/plain;charset=UTF-8 türüyle gider.
  • FormData ile Content-Type yazmak: elle yazılan başlık sınır değerini içermediği için sunucu gövdeyi ayrıştıramaz.
  • Gövdeyi iki kez okumak: ikinci okuma TypeError verir; gerekiyorsa önce clone() kullanın.
  • Aynı AbortController'ı tekrar kullanmak: bir kez iptal edilen sinyal sonraki istekleri de hemen iptal eder.
  • CORS hatasını kodla çözmeye çalışmak: izin sunucu tarafında verilir; ayrıntılar CORS hataları yazısında.

Sık Sorulan Sorular

fetch() ile XMLHttpRequest arasında hangisini kullanmalıyım?

MDN, Fetch API'yi XMLHttpRequest'in modern ve daha esnek karşılığı olarak tanımlar: söz tabanlıdır, service worker ve CORS gibi özelliklerle bütünleşiktir. Yükleme ilerlemesini olaylarla izlemek gibi özel bir ihtiyacınız yoksa yeni kodda fetch() tercih edilir. Karşılaştırma için XMLHttpRequest ve fetch farkı yazısına bakabilirsiniz.

fetch() isteği neden 404 olmasına rağmen catch bloğuna düşmüyor?

Çünkü sunucu bir yanıt döndürmüştür ve ağ açısından istek başarılıdır. fetch() sözü yalnızca ağ hatası, geçersiz URL, geçersiz seçenek veya iptal gibi durumlarda reddedilir. Hata durum kodlarını yakalamak için if (!response.ok) throw ... satırını eklemeniz gerekir.

fetch() isteğine nasıl zaman aşımı eklerim?

signal: AbortSignal.timeout(5000) seçeneğini verin. Süre dolduğunda söz TimeoutError ile reddedilir. Hem kullanıcı iptali hem zaman aşımı istiyorsanız AbortSignal.any([controller.signal, AbortSignal.timeout(5000)]) ile iki sinyali birleştirebilirsiniz.

fetch() çerezleri otomatik gönderir mi?

Aynı kökene giden isteklerde evet, çünkü credentials varsayılanı same-origin'dir. Başka bir kökene çerez göndermek için credentials: "include" gerekir ve sunucunun CORS başlıklarıyla buna izin vermesi şarttır. Çerezin SameSite özelliği Strict veya Lax ise çerez yine de siteler arası isteklerde gönderilmez.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Web API öğrenme rehberi · Request, Response ve Headers · CORS nedir · XMLHttpRequest ve fetch farkı · FormData ile form gönderme · Node.js ile fetch