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:
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

Bir XHR nesnesi her an beş durumdan birindedir ve durum her değiştiğinde readystatechange olayı tetiklenir:
| Değer | Sabit | Anlamı |
|---|---|---|
| 0 | UNSENT | Nesne oluşturuldu, open() henüz çağrılmadı |
| 1 | OPENED | open() çağrıldı; başlıklar ayarlanabilir, send() çağrılabilir |
| 2 | HEADERS_RECEIVED | send() çağrıldı, yönlendirmeler izlendi, başlıklar ve durum geldi |
| 3 | LOADING | Gövde iniyor; metin türünde responseText kısmi veri tutar |
| 4 | DONE | İşlem bitti; başarılı da olabilir, başarısız da |
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:
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: 200Form kodlamalı gövdeyi elle yazmak yerine URLSearchParams ile üretebilirsiniz:
const govde = new URLSearchParams({ foo: "bar", lorem: "ipsum" }).toString();
console.log(govde); // foo=bar&lorem=ipsumYanı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:
| responseType | response değeri |
|---|---|
| boş metin veya text | Metin; varsayılan |
| arraybuffer | İkili veri içeren ArrayBuffer |
| blob | İkili veri içeren Blob |
| document | MIME türüne göre HTML Document veya XMLDocument |
| json | JSON çö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:
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

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:
| Olay | Ne zaman tetiklenir |
|---|---|
| loadstart | İstek veri yüklemeye başladığında |
| progress | Yeni veri geldikçe düzenli aralıklarla |
| load | İstek başarıyla tamamlandığında |
| error | İstek bir hatayla karşılaştığında |
| abort | abort() ile iptal edildiğinde |
| timeout | timeout süresi dolduğunda |
| loadend | Baş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:
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 bilinmiyorBu 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:
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.
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:
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")); // 6502Son 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:
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:
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&12345Fetch 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ı

| Konu | XMLHttpRequest | fetch() |
|---|---|---|
| Sonuç alma | Olaylar ve geri çağırma | Promise ve Response nesnesi |
| HTTP hata kodu | status ile kontrol edilir | Söz reddedilmez, ok ile kontrol edilir |
| İptal | abort() | AbortController ve signal |
| Zaman aşımı | timeout özelliği ve timeout olayı | AbortSignal.timeout() |
| Yükleme ilerlemesi | upload üzerindeki progress olayı | Yerleşik ilerleme olayı yok |
| Yanıt biçimi | open sonrası responseType ile seçilir | json(), text(), blob() gibi yöntemlerle seçilir |
| Kimlik bilgisi | withCredentials | credentials seçeneği |
| Senkron istek | async false ile mümkün ama kullanımdan kaldırıldı | Yok, her zaman asenkron |
| Akış olarak okuma | LOADING durumunda kısmi responseText | response.body bir ReadableStream |
| Service worker | fetch kadar bütünleşik değil | fetch 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:
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
progressolayları tetiklenmez. - setRequestHeader() çağrısını open() öncesine koymak: başlıklar yalnızca
OPENEDdurumunda ayarlanabilir. - Yalnızca readyState 4 kontrolü yapmak: hata durumunda da istek
DONEolur;statusdeğ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