XMLHttpRequest (XHR), sayfayı yenilemeden bir sunucudan veri almanızı sağlayan eski ama hâlâ desteklenen tarayıcı API'sidir. Adında XML geçse de her tür veriyle çalışır. Temel akış dört adımdır: nesneyi oluşturursunuz, open() ile yöntemi ve URL'yi belirlersiniz, sonucu almak için olay dinleyicileri eklersiniz ve send() ile isteği gönderirsiniz. MDN yeni projeler için söz tabanlı Fetch API'yi daha esnek ve güçlü bir karşılık olarak önerir; XHR ise eski kodları okumak ve yükleme ilerlemesini olaylarla izlemek gibi durumlarda karşınıza çıkar.

Bu yazıda XHR'nin durumlarını (readyState), yanıt türlerini, ilerleme olaylarını, zaman aşımını, kimlik bilgilerini ve senkron isteklerin neden kaçınılması gereken bir yöntem olduğunu göreceksiniz. Sonunda XHR ile fetch() arasındaki farkları bir tabloda karşılaştırıyoruz. XHR örnekleri yalnızca tarayıcıda çalıştığı için MDN belgelerine göre yazıldı; başlık ayrıştırma gibi ağ gerektirmeyen yardımcı kodlar Node.js 24 ile denendi.

İlk XHR isteği

MDN'deki en kısa örnek, bir metin dosyasını indirip load olayında yanıt metnini konsola yazar. Dinleyici içinde this, isteğin kendisini gösterir:

javascript
function reqListener() {
  console.log(this.responseText);
}

const req = new XMLHttpRequest();
req.addEventListener("load", reqListener);
req.open("GET", "http://www.example.org/example.txt");
req.send();

open(method, url, async, user, password) isteği hazırlar. Yöntem GET, POST, PUT, DELETE gibi bir HTTP yöntemidir. Üçüncü argüman async varsayılan olarak true'dur; false verildiğinde istek senkron olur ve send() yanıt gelene kadar dönmez. Kullanıcı adı ve parola isteğe bağlıdır. Zaten açılmış bir istek için open()'ı tekrar çağırmak, abort() çağırmakla aynı etkiyi yapar.

send(body) isteği gönderir. Gövde bir Document, Blob, ArrayBuffer, typed array, DataView, FormData, URLSearchParams, metin veya null olabilir. Yöntem GET veya HEAD ise gövde yok sayılır. setRequestHeader() ile bir Accept başlığı belirlemediyseniz tarayıcı Accept: */* gönderir. Aynı istekte send()'i ikinci kez çağırmak InvalidStateError fırlatır.

readyState ve readystatechange

readyState ve readystatechange: UNSENT, OPENED, HEADERS_RECEIVED, LOADING

Bir XHR nesnesi her an beş durumdan birindedir ve durum her değiştiğinde readystatechange olayı tetiklenir:

DeğerSabitAnlamı
0UNSENTNesne oluşturuldu, open() henüz çağrılmadı
1OPENEDopen() çağrıldı; başlıklar ayarlanabilir, send() çağrılabilir
2HEADERS_RECEIVEDsend() çağrıldı, yönlendirmeler izlendi, başlıklar ve durum geldi
3LOADINGGövde iniyor; metin türünde responseText kısmi veri tutar
4DONEİşlem bitti; başarılı da olabilir, başarısız da
javascript
const xhr = new XMLHttpRequest();
xhr.open("POST", "/server", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = () => {
  if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
    // İstek tamamlandı, yanıtı burada işleyin
  }
};
xhr.send("foo=bar&lorem=ipsum");

DONE durumu tek başına başarı anlamına gelmez; mutlaka status değerine de bakmalısınız. İstek tamamlanmadan önce status 0'dır ve tarayıcılar XHR hatalarında da 0 bildirir. MDN'nin örneğinde bir isteğin aşamalara göre durum kodu şöyle ilerler:

javascript
const xhr = new XMLHttpRequest();
console.log("UNSENT: ", xhr.status);
xhr.open("GET", "/server");
console.log("OPENED: ", xhr.status);
xhr.onprogress = () => {
  console.log("LOADING: ", xhr.status);
};
xhr.onload = () => {
  console.log("DONE: ", xhr.status);
};
xhr.send();
// UNSENT: 0
// OPENED: 0
// LOADING: 200
// DONE: 200

Form kodlamalı gövdeyi elle yazmak yerine URLSearchParams ile üretebilirsiniz:

javascript
const govde = new URLSearchParams({ foo: "bar", lorem: "ipsum" }).toString();
console.log(govde); // foo=bar&lorem=ipsum

Yanıt türleri: responseType ve response

responseType, yanıtın hangi biçimde sunulacağını belirler ve open() ile send() arasında ayarlanır. Sonuç response özelliğinden okunur:

responseTyperesponse değeri
boş metin veya textMetin; varsayılan
arraybufferİkili veri içeren ArrayBuffer
blobİkili veri içeren Blob
documentMIME türüne göre HTML Document veya XMLDocument
jsonJSON çözülerek elde edilen JavaScript nesnesi

Sunucunun gönderdiği veri seçtiğiniz türle uyumlu değilse response değeri null olur. responseText yalnızca metin türlerinde anlamlıdır; responseXML ise yanıtı XML veya HTML olarak ayrıştırılmış bir belge olarak verir ve worker içinde kullanılamaz. İkili veri almak için MDN'nin önerdiği modern yol responseType = "arraybuffer"'dır:

javascript
const req = new XMLHttpRequest();
req.onload = (e) => {
  const arraybuffer = req.response; // responseText değil
};
req.open("GET", url);
req.responseType = "arraybuffer";
req.send();

İlerleme olayları: indirme ve yükleme

İlerleme olayları: indirme ve yükleme: loadstart, progress, load, error

XHR'nin fetch()'ten ayrıldığı noktalardan biri, aktarım ilerlemesini olaylarla izleyebilmesidir. Olaylar XMLHttpRequestEventTarget arayüzünden gelir ve hem istek nesnesinde hem de xhr.upload nesnesinde bulunur:

OlayNe zaman tetiklenir
loadstartİstek veri yüklemeye başladığında
progressYeni veri geldikçe düzenli aralıklarla
loadİstek başarıyla tamamlandığında
errorİstek bir hatayla karşılaştığında
abortabort() ile iptal edildiğinde
timeouttimeout süresi dolduğunda
loadendBaşarı, hata veya iptal fark etmeksizin istek bittiğinde

İndirme ilerlemesi istek nesnesinde, yükleme ilerlemesi xhr.upload üzerinde dinlenir. MDN, dinleyicilerin open() çağrılmadan önce eklenmesi gerektiğini, aksi halde progress olaylarının tetiklenmeyeceğini belirtir. progress olayındaki loaded aktarılan bayt sayısını, total toplam boyutu verir; lengthComputable false ise toplam bilinmiyordur ve total sıfırdır:

javascript
function yuzde(event) {
  if (!event.lengthComputable) return "toplam boyut bilinmiyor";
  return `%${Math.round((event.loaded / event.total) * 100)}`;
}
console.log(yuzde({ lengthComputable: true, loaded: 512, total: 2048 })); // %25
console.log(yuzde({ lengthComputable: false, loaded: 512, total: 0 })); // toplam boyut bilinmiyor

Bu fonksiyonu bir dosya yüklemesinde şöyle kullanırsınız. FormData ile form ve dosya göndermenin ayrıntıları FormData yazısında, seçilen dosyayı okumak ise File API yazısında anlatılıyor:

javascript
const xhr = new XMLHttpRequest();
const durum = document.querySelector("#durum");

xhr.upload.addEventListener("progress", (e) => {
  durum.textContent = yuzde(e);
});
xhr.addEventListener("load", () => {
  durum.textContent = `Bitti: ${xhr.status}`;
});
xhr.addEventListener("error", () => {
  durum.textContent = "Yükleme başarısız";
});

xhr.open("POST", "/upload");
xhr.send(new FormData(document.querySelector("form")));

CORS açısından bir ayrıntıya dikkat edin: xhr.upload üzerine dinleyici eklemek, başka kökene giden bir isteğin basit istek sayılmasını engeller ve ön uçuş isteğini tetikler. Bu kural CORS rehberindeki basit istek koşulları arasında yer alır. loadend olayı ise bitişin nedenini söylemez; yalnızca her durumda çalışması gereken temizlik işleri için uygundur.

Zaman aşımı ve iptal

timeout özelliği, isteğin otomatik sonlandırılmadan önce sürebileceği milisaniye sayısıdır; varsayılan 0 zaman aşımı olmadığı anlamına gelir. Süre dolduğunda timeout olayı tetiklenir. abort() ise gönderilmiş bir isteği iptal eder: readyState 0'a, status 0'a döner ve istek sürüyorsa sırasıyla readystatechange, abort ve loadend olayları gönderilir.

javascript
const xhr = new XMLHttpRequest();
xhr.open("GET", "/server", true);
xhr.timeout = 2000;
xhr.onload = () => {
  // İstek tamamlandı
};
xhr.ontimeout = (e) => {
  // 2 saniye içinde yanıt gelmedi
};
xhr.send(null);

Başlıklar ve kimlik bilgileri

setRequestHeader() mutlaka open()'dan sonra, send()'den önce çağrılmalıdır. Aynı başlık için birden çok kez çağırırsanız değerler tek bir başlıkta birleştirilir. Tarayıcının yönettiği yasaklı başlıkları ayarlama denemeleri uyarı veya hata vermeden yok sayılır. Authorization başlığı eklenebilir ama istek başka kökene yönlendirilirse kaldırılır. Başka kökene özel başlık gönderdiğinizde sunucunun Access-Control-Allow-Headers ile izin vermesi gerekir.

withCredentials = true, başka kökene giden isteklerde çerez, kimlik doğrulama başlığı veya TLS istemci sertifikası gönderilmesini sağlar; aynı köken isteklerini hiç etkilemez. Varsayılanı false'tur ve başka bir alan adından gelen XHR yanıtı, bu özellik true olmadan kendi alanı için çerez ayarlayamaz.

Yanıt başlıklarını tek tek almak için getResponseHeader(ad), hepsini birden almak için getAllResponseHeaders() kullanılır. İkincisi Set-Cookie dışındaki tüm başlıkları CRLF ile ayrılmış tek bir metin olarak döndürür; modern tarayıcılarda başlık adları küçük harftir. MDN'nin bu metni bir nesneye dönüştüren kodunu örnek bir başlık metniyle denedik:

javascript
const ham =
  "date: Fri, 08 Dec 2017 21:04:30 GMT\r\n" +
  "content-encoding: gzip\r\n" +
  "content-type: text/html; charset=utf-8\r\n" +
  "vary: Cookie, Accept-Encoding\r\n" +
  "content-length: 6502\r\n";
// Tarayıcıda: const ham = request.getAllResponseHeaders();

const satirlar = ham.trim().split(/[\r\n]+/);
const headerMap = {};
satirlar.forEach((line) => {
  const parts = line.split(": ");
  const header = parts.shift();
  const value = parts.join(": ");
  headerMap[header] = value;
});
console.log(satirlar.length); // 5
console.log(headerMap["content-type"]); // text/html; charset=utf-8

const basliklar = new Headers(Object.entries(headerMap));
console.log(basliklar.get("Content-Length")); // 6502

Son satırda nesneyi bir Headers nesnesine çevirdik; böylece fetch tarafındaki gibi büyük küçük harf duyarsız get() kullanabilirsiniz.

Yalnızca başlıkları istemek: HEAD

Bir dosyanın içeriğine değil yalnızca üst bilgisine ihtiyacınız varsa HEAD yöntemi gövdeyi indirmeden başlıkları getirir. MDN'nin örneği, bir sayfanın son değiştirilme tarihini Last-Modified başlığından okur. Başlık yoksa getResponseHeader() null döndürür; yanıt henüz gelmemişse de sonuç null olur:

javascript
function getHeaderTime() {
  console.log(this.getResponseHeader("Last-Modified"));
}

const req = new XMLHttpRequest();
req.open("HEAD", "your-page.html");
req.onload = getHeaderTime;
req.send();

Aynı rehber bu değeri localStorage içinde saklayıp sonraki ziyarette karşılaştırarak sayfanın değişip değişmediğini anlamayı da gösterir. Tarayıcı depolaması için localStorage ve sessionStorage yazısına bakabilirsiniz. Daha eski kodlarda ikili veri almak için overrideMimeType("text/plain; charset=x-user-defined") çağrısıyla yanıtın ham metin olarak alındığını da görebilirsiniz; MDN bu yöntemin çalıştığını ama responseType değerlerinin bu işi çok daha kolaylaştırdığını belirtir.

Senkron istekler neden kullanılmamalı?

open()'a üçüncü argüman olarak false verildiğinde istek senkron olur ve yanıt gelene kadar kodun çalışması durur. MDN, senkron XHR'nin özellikle zayıf ağlarda veya yavaş sunucularda sayfanın donmasına yol açtığını, kullanımdan kaldırıldığını (deprecated) ve asenkron isteklerle değiştirilmesi gerektiğini belirtir. Ana iş parçacığında senkron istekler kullanıcı deneyimini bozar ve birçok tarayıcı bu desteği tamamen kaldırmıştır; senkron isteklere hâlâ worker içinde izin verilir. timeout gibi yeni XHR özellikleri senkron isteklerle kullanılamaz ve InvalidAccessError fırlatır; belge bağlamında senkron bir istekte responseType değiştirmek de aynı hatayı verir. Worker kullanımı için Web Workers yazısına bakabilirsiniz.

Önbelleği atlamak

MDN'nin XHR rehberi, tarayıcı önbelleğini atlamak için URL'ye bir zaman damgası eklemeyi gösterir: önbellek URL'ye göre indekslendiği için her istek benzersiz olur. Aşağıdaki fonksiyon, URL'de zaten soru işareti varsa &, yoksa ? ekler; tarayıcıda zaman değeri olarak new Date().getTime() kullanılır:

javascript
function onbellekAtla(url, zaman) {
  return url + (/\?/.test(url) ? "&" : "?") + zaman;
}
console.log(onbellekAtla("http://example.com/bar.html", 12345));
// http://example.com/bar.html?12345
console.log(onbellekAtla("http://example.com/bar.html?foobar=baz", 12345));
// http://example.com/bar.html?foobar=baz&12345

Fetch API'de aynı amaç için URL'yi değiştirmek gerekmez; cache: "no-store" veya cache: "reload" seçeneği yeterlidir. Bu modlar Request ve Response yazısında açıklanıyor.

XMLHttpRequest ile fetch() karşılaştırması

XMLHttpRequest ile fetch() karşılaştırması: Sonuç alma, HTTP hata kodu, İptal, Zaman aşımı
KonuXMLHttpRequestfetch()
Sonuç almaOlaylar ve geri çağırmaPromise ve Response nesnesi
HTTP hata kodustatus ile kontrol edilirSöz reddedilmez, ok ile kontrol edilir
İptalabort()AbortController ve signal
Zaman aşımıtimeout özelliği ve timeout olayıAbortSignal.timeout()
Yükleme ilerlemesiupload üzerindeki progress olayıYerleşik ilerleme olayı yok
Yanıt biçimiopen sonrası responseType ile seçilirjson(), text(), blob() gibi yöntemlerle seçilir
Kimlik bilgisiwithCredentialscredentials seçeneği
Senkron istekasync false ile mümkün ama kullanımdan kaldırıldıYok, her zaman asenkron
Akış olarak okumaLOADING durumunda kısmi responseTextresponse.body bir ReadableStream
Service workerfetch kadar bütünleşik değilfetch olayı ve Cache API ile bütünleşik

Promise tabanlı bir arayüz istiyor ama yükleme ilerlemesi için XHR kullanmanız gerekiyorsa, XHR'yi bir Promise içine sarabilirsiniz. Aşağıdaki sarmalayıcı yalnızca tarayıcıda çalışır:

javascript
function xhrIstek(method, url, body = null) {
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    xhr.responseType = "json";
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        resolve(xhr.response);
      } else {
        reject(new Error(`HTTP ${xhr.status}`));
      }
    };
    xhr.onerror = () => reject(new TypeError("Ağ hatası"));
    xhr.ontimeout = () => reject(new Error("Zaman aşımı"));
    xhr.send(body);
  });
}

Sık yapılan hatalar

  • Dinleyicileri open() sonrasında eklemek: MDN'ye göre progress olayları tetiklenmez.
  • setRequestHeader() çağrısını open() öncesine koymak: başlıklar yalnızca OPENED durumunda ayarlanabilir.
  • Yalnızca readyState 4 kontrolü yapmak: hata durumunda da istek DONE olur; status değerini de kontrol edin.
  • responseType json iken responseText okumak: bu durumda response özelliğini kullanın.
  • Senkron istek kullanmak: sayfa donar ve yeni özellikler (timeout, abort) çalışmaz.
  • status 0 değerini başarı sanmak: 0 genellikle isteğin yapılamadığını, iptal edildiğini veya CORS gibi bir nedenle engellendiğini gösterir; nedeni konsolda arayın.

Sık Sorulan Sorular

XMLHttpRequest kullanımdan kaldırıldı mı?

Hayır, XMLHttpRequest arayüzünün kendisi kullanımdadır. Kullanımdan kaldırılan, ana iş parçacığındaki senkron XHR'dir. Bununla birlikte MDN, modern web uygulamalarında genellikle Fetch API'nin kullanıldığını belirtir.

AJAX ile XMLHttpRequest aynı şey mi?

AJAX, sayfanın tamamını yenilemeden sunucudan veri alıp sayfanın bir kısmını güncelleme yaklaşımının adıdır. XHR bu yaklaşımın klasik aracıdır; aynı iş bugün çoğunlukla fetch() ile yapılır.

fetch() ile yükleme ilerlemesi gösterebilir miyim?

Fetch API'de xhr.upload benzeri yerleşik bir yükleme ilerleme olayı yoktur. Yükleme çubuğu gösterecekseniz XHR'nin upload olayları hâlâ en doğrudan yoldur.

XHR ile JSON yanıtını nasıl alırım?

open() çağrısından sonra xhr.responseType = "json" ayarlayın ve load olayında xhr.response değerini okuyun; bu değer zaten çözülmüş bir JavaScript nesnesidir. Sunucu geçerli JSON göndermezse response null olur, bu yüzden değeri kullanmadan önce kontrol edin. Alternatif olarak varsayılan metin türünde kalıp JSON.parse(xhr.responseText) da kullanabilirsiniz.

XHR isteği CORS kurallarına tabi mi?

Evet. Başka kökene giden XHR istekleri de aynı köken politikasına ve CORS başlıklarına tabidir. Hata mesajlarının çözümü için CORS hataları rehberine bakın.

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 · Fetch API kullanımı · Request, Response ve Headers · FormData ile form gönderme · CORS nedir · Web Workers