Bir HTTP mesajı, istemci ile sunucu arasında gidip gelen veri birimidir ve iki türü vardır: istemcinin gönderdiği istek ve sunucunun döndürdüğü yanıt. HTTP/1.1 biçiminde ikisi de aynı iskeleti izler: tek satırlık bir başlangıç satırı, isteğe bağlı başlıklar, başlıkların bittiğini gösteren boş bir satır ve isteğe bağlı bir gövde. İstekteki başlangıç satırının ilk kelimesi olan metot (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS...) isteğin anlamını belirler; metotlar güvenli, idempotent ve önbelleklenebilir olup olmamalarına göre ayrılır.

Bu yazı mesajın parçalarını, istek hedefi biçimlerini, metotların tek tek davranışını ve güvenli ile idempotent kavramlarının pratikteki karşılığını anlatıyor. Örneklerdeki Request, Response ve Headers nesneleri tarayıcıda ve Node.js'te aynı standartla çalıştığı için kodlar ağ isteği yapmadan denendi. Durum kodlarının ayrıntısı HTTP durum kodları yazısında, önbellek başlıkları Cache-Control ve ETag yazısında; serinin haritası için Web API öğrenme rehberi sayfasına bakın.

Bir HTTP mesajının anatomisi

MDN'in kılavuzu mesajları okunabilir olduğu için HTTP/1.1 biçimiyle anlatır. Bir mesajın dört parçası vardır. Başlangıç satırı HTTP sürümünü ve istekte metodu, yanıtta sonucu bildirir. Başlıklar mesajı tanımlayan meta verilerdir; örneğin istek kabul ettiği biçimleri, yanıt gerçekte döndürdüğü biçimi söyler. Boş satır meta verinin bittiğini gösterir. Gövde ise mesajla taşınan veridir: istekte form verisi, yanıtta istenen kaynak. Mesajın gövde taşıyıp taşımadığına başlangıç satırı ve başlıklar karar verir. Başlangıç satırı ile başlıkların toplamına mesajın başı (head), sonrasına gövde (body) denir.

Geliştiriciler HTTP mesajlarını nadiren elle kurar; tarayıcı, proxy ve web sunucusu bu işi yapan yazılımlar içerir. Siz mesajı tarayıcıda fetch() gibi API'lerle, sunucuda ise yapılandırma dosyalarıyla şekillendirirsiniz. Gerçek mesajları tarayıcı geliştirici araçlarının Ağ (Network) sekmesinde ya da curl gibi komut satırı araçlarıyla görebilirsiniz.

İstek mesajı

İstek mesajı: İstek hedefinin dört biçimi, İstek başlıkları ve gövde

MDN'in örneği, bir web sayfasındaki form gönderildiğinde oluşan bir POST isteğidir:

http
POST /users HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 49

name=FirstName+LastName&email=bsmth%40example.com

İlk satıra istek satırı denir ve üç parçadan oluşur: metot, istek hedefi ve protokol sürümü. Protokol neredeyse her zaman HTTP/1.1'dir, çünkü 0.9 ve 1.0 kullanımdan kalkmıştır. HTTP/2 ve sonrasında sürüm mesajda yer almaz, bağlantı kurulumundan anlaşılır.

İstek hedefinin dört biçimi

BiçimÖrnekKullanım
origin formGET /docs/sayfa?x=1 HTTP/1.1Mutlak yol, Host başlığıyla birleştirilir; GET, POST, HEAD, OPTIONS
absolute formGET https://example.com/docs HTTP/1.1Proxy'ye bağlanırken tam URL ile GET
authority formCONNECT example.com:443 HTTP/1.1Yalnızca CONNECT ile HTTP tüneli kurarken
asterisk formOPTIONS * HTTP/1.1Yalnızca OPTIONS ile sunucunun bütününü kastederken

En yaygını origin form'dur: alıcı, mutlak yolu Host başlığındaki bilgiyle birleştirir; yola genellikle anahtar=değer biçiminde bir sorgu dizesi eklenebilir.

İstek başlıkları ve gövde

HTTP/1.x'te her başlık, büyük küçük harfe duyarsız bir ad, iki nokta üst üste ve biçimi başlığa göre değişen bir değerden oluşur; başlık değeriyle birlikte tek bir satırdır ve Cookie gibi bazı başlıklarda bu satır oldukça uzayabilir. MDN başlıkları kabaca iki gruba ayırır: isteğe bağlam ya da ek mantık katan istek başlıkları (koşullu istekler gibi) ve gövde varsa verinin özgün biçimini ve uygulanan kodlamayı anlatan temsil başlıkları (Content-Type gibi).

MDN'e göre istek gövdesi yalnızca PATCH, POST ve PUT isteklerinde bulunur. Gövde, form örneğindeki gibi anahtar=değer çiftleri, bir JSON nesnesi ya da sınır (boundary) dizesiyle ayrılmış çok parçalı veri olabilir. HTML formlarında gövdenin biçimini formun enctype özniteliği belirler: varsayılan application/x-www-form-urlencoded, dosya ve büyük veri için multipart/form-data, bir de text/plain. fetch() ile gönderilen POST isteklerinde gövde herhangi bir türde olabilir. Form tarafının JavaScript ayrıntıları form gönderme ve FormData yazısında, HTML tarafı HTML öğrenme rehberi serisinde.

Yanıt mesajı

Yanıt, isteğin sonucunu istemciye bildirir. Yeni kullanıcı oluşturan bir POST isteğine dönen yanıt şöyle görünebilir:

http
HTTP/1.1 201 Created
Content-Type: application/json
Location: http://example.com/users/123

{
  "message": "New user created",
  "user": { "id": 123 }
}

Yanıttaki ilk satıra durum satırı denir: protokol sürümü, sayısal durum kodu ve isteğe bağlı gerekçe ifadesi (reason phrase). Gerekçe ifadesi yalnızca insanlara yardımcı olan bilgilendirici bir metindir; programınız kararlarını her zaman sayısal koda göre vermelidir. Yanıt başlıkları da iki gruba ayrılır: Server, Date, Cache-Control gibi sonraki istekleri ve mesajın bağlamını etkileyenler ile gövdenin biçimini anlatan temsil başlıkları.

Yanıt gövdesi tek bir kaynaksa Content-Type ve Content-Length ile tanımlanır; uzunluğu bilinmiyorsa Transfer-Encoding: chunked ile parçalar halinde gönderilebilir. Çok parçalı gövdeler genellikle formlar ve aralık (range) istekleriyle ilişkilidir. MDN, isteği içerik göndermeden yanıtlayan 201 ve 204 gibi durum kodlarında gövde bulunmadığını belirtir; yukarıdaki örnek ise 201 yanıtının bilgilendirici bir gövdeyle de gönderilebildiğini gösteriyor. Yeni kaynağın adresi Location başlığıyla bildirilir.

Kodla bir mesaj kurmak

Tarayıcıda bir isteğin metodunu, başlıklarını ve gövdesini Request nesnesi ya da doğrudan fetch() seçenekleriyle belirlersiniz. Aşağıdaki örnek ağa çıkmadan bir form isteği kurar, gövdeyi okur ve Headers nesnesinin başlık adlarını küçük harfe çevirip değerlerin başındaki ve sonundaki boşlukları kırptığını gösterir. Son satır, gövdeli bir GET isteği kurmaya çalışınca TypeError fırlatıldığını gösterir; MDN'in Fetch kılavuzu GET isteğine gövde eklenemeyeceğini belirtir.

javascript
const form = new Request('https://example.com/users', {
  method: 'POST',
  body: new URLSearchParams({ name: 'Ayşe Yılmaz', email: 'ayse@example.com' }),
});
console.log(form.method, form.headers.get('Content-Type'));
// POST application/x-www-form-urlencoded;charset=UTF-8
console.log(await form.text());
// name=Ay%C5%9Fe+Y%C4%B1lmaz&email=ayse%40example.com

const basliklar = new Headers({ 'Content-Type': '  application/json  ', 'X-Istek-No': '42' });
console.log(JSON.stringify([...basliklar]));
// [["content-type","application/json"],["x-istek-no","42"]]

try {
  new Request('https://example.com/ara', { method: 'GET', body: 'q=1' });
} catch (hata) {
  console.log(hata.name); // TypeError
}

URLSearchParams gövdesi verildiğinde Content-Type otomatik olarak application/x-www-form-urlencoded;charset=UTF-8 olur ve Türkçe karakterler yüzde kodlamasıyla gönderilir. Bu yüzden ikili veri için bu tür uygun değildir; MDN dosyalar için multipart/form-data önerir. Ayrıca tarayıcının kendisinin belirlediği bazı başlıklar (yasaklı istek başlıkları) betikle ayarlanamaz. Bu nesnelerin ayrıntıları Request, Response ve Headers yazısında.

Güvenli, idempotent ve önbelleklenebilir

Güvenli, idempotent ve önbelleklenebilir: Hayır, HEAD, POST, Evet

MDN her metodun sayfasında aynı özellik tablosunu verir. Güvenli (safe) bir metot sunucudaki durumu değiştirmek için değil, yalnızca veri almak için kullanılır; GET sayfası bu metodun sadece veri istemek için kullanılması gerektiğini söyler. İdempotent bir metotta aynı isteği bir kez göndermek ile art arda birkaç kez göndermek arasında sonuç açısından fark yoktur; MDN bunu PUT ile POST arasındaki temel fark olarak anlatır: aynı POST isteğini tekrarlamak örneğin aynı siparişi birkaç kez oluşturabilir. Önbelleklenebilir ise yanıtın saklanıp sonraki isteklerde kullanılabileceğini gösterir.

Metotİstek gövdesiBaşarılı yanıt gövdesiGüvenliİdempotentÖnbelleklenebilirHTML formunda
GETHayırEvetEvetEvetEvetEvet
HEADHayırHayırEvetEvetEvetHayır
POSTEvetEvetHayırHayırYalnızca tazelik bilgisi varsaEvet
PUTEvetOlabilirHayırEvetHayırHayır
PATCHEvetOlabilirHayırHayırYalnızca tazelik bilgisi varsaHayır
DELETEHayırOlabilirHayırEvetHayırHayır
OPTIONSOlabilir (tanımsız anlam)OlabilirEvetEvetHayırHayır
TRACEHayırEvetEvetEvetHayırHayır
CONNECTHayırHayırHayırHayırHayırHayır

Bu özelliklerin pratik bir karşılığı vardır. Bağlantı koptuğunda idempotent bir isteği yeniden denemek güvenlidir, idempotent olmayan bir POST'u körü körüne yeniden denemek ise çift kayıt yaratabilir. Güvenli metotlar sunucuda değişiklik yapmamalıdır; bu kural CSRF korumasının da temelidir, çünkü tarayıcının kendiliğinden gönderdiği GET istekleri bir eylemi tetiklememelidir. İdempotent olmak, sunucunun her seferinde aynı yanıtı döndüreceği anlamına gelmez: aynı kaynağı iki kez silmek kaynağın son durumunu değiştirmez, ama ikinci yanıt farklı bir kod taşıyabilir.

Metotlar tek tek

GET ve HEAD

GET belirtilen kaynağın bir temsilini ister ve gövde içermemelidir. MDN, GET isteğinde gövde göndermenin anlamının tanımsız olduğunu ve bazı sunucuların böyle bir isteği 4xx hatasıyla reddedebileceğini not eder. Veri göndermeniz gerekiyorsa sorgu dizesi kullanırsınız. HEAD ise GET kullanılsaydı gönderilecek başlıkları gövde olmadan ister. Büyük bir dosyayı indirmeden önce Content-Length ile boyutunu öğrenmek tipik kullanımıdır. HEAD yanıtı önbellekteki bir kopyanın eskidiğini gösterirse, GET yapılmamış olsa bile o kopya geçersiz sayılır. Yanıtta yanlışlıkla gövde bulunursa bu gövde yok sayılmalıdır.

POST

POST sunucuya veri gönderir; gövdenin türünü Content-Type başlığı belirtir. HTML formları veriyi genellikle bu metotla yollar ve bu çoğunlukla sunucuda bir değişikliğe yol açar. MDN'in saydığı tipik işler arasında mevcut kaynaklara not eklemek, bir ileti panosuna mesaj göndermek, kayıt formuyla yeni kullanıcı eklemek ve veritabanına ekleme yapmak bulunur. POST idempotent değildir.

PUT ve PATCH

PUT yeni bir kaynak oluşturur ya da hedef kaynağın temsilini istekteki içerikle tamamen değiştirir. Kaynak yoksa ve oluşturulduysa sunucu 201 göndermelidir; varsa ve başarıyla güncellendiyse 200 veya 204. PATCH ise kaynağa kısmi değişiklik uygular: PUT tam bir yerine koyma, PATCH bir değişiklik talimatları kümesidir. MDN farkı sayaç örneğiyle anlatır: kendiliğinden artan bir sayaç içeren kaynakta PUT sayacın üzerine yazar, PATCH yazmayabilir; bu yüzden PATCH her zaman idempotent değildir. Sunucu PATCH desteğini Allow veya CORS için Access-Control-Allow-Methods listesinde, ayrıca kabul ettiği ortam türlerini Accept-Patch başlığıyla duyurabilir.

DELETE

DELETE belirtilen kaynağın silinmesini ister ve GET gibi gövde içermemelidir; gövdenin anlamı tanımsızdır. Başarılı yanıt için birkaç seçenek vardır: ek bilgi gerekmiyorsa 204, sonucu anlatan bir gövdeyle 200, istek kabul edildi ama silme henüz yapılmadıysa 202. MDN, tarayıcının PUT ve DELETE metotlarını kullanıcı eylemleri için kendiliğinden kullanmadığını, geliştiricilerin bunları fetch() ile ayarlayabileceğini belirtir.

OPTIONS

OPTIONS bir URL ya da (* ile) sunucunun tamamı için izin verilen iletişim seçeneklerini sorar. Yanıt genellikle Allow başlığıyla desteklenen metotları listeler. Tarayıcıların en sık kullandığı yer CORS ön kontrol (preflight) isteğidir: tarayıcı, asıl isteğin metodunu Access-Control-Request-Method, ek başlıklarını Access-Control-Request-Headers ile bildirir ve sunucunun Access-Control-Allow-* başlıklarıyla izin vermesini bekler. Bu akışın tamamı CORS nedir yazısında anlatılıyor.

TRACE, CONNECT ve QUERY

TRACE, hedef kaynağa giden yol boyunca bir geri döngü testi yapar: son alıcı aldığı mesajı message/http türünde bir 200 yanıtının gövdesinde geri yansıtır. İstemci gövde ya da kimlik bilgisi, çerez gibi hassas başlıklar göndermemelidir; MDN, güvenlik kaygıları nedeniyle bazı sunucuların bu metodu kapattığını ve 405 döndürdüğünü belirtir. CONNECT bir proxy'den hedef sunucuya HTTP tüneli kurmasını ister; istek hedefi yalnızca ana-makine:port biçimindedir ve port zorunludur. MDN, CONNECT destekleyen bir proxy işletiyorsanız bunu bilinen portlarla sınırlamanızı, aksi halde örneğin SMTP trafiğinin spam aktarmak için kötüye kullanılabileceğini uyarır.

QUERY, istek içeriğinde taşınan bir sorguyu güvenli ve idempotent biçimde çalıştırmayı isteyen bir metottur. POST gibi gövde taşır, ama POST'tan farklı olarak açıkça güvenli ve idempotenttir; sorgu URL'de değil gövdede olduğu için URI uzunluk sınırlarına takılmaz. MDN, URL'ye sığan küçük sorgular için yer imine eklenebilir ve kolay önbelleklenebilir olduğundan GET'in hâlâ iyi bir seçim olduğunu belirtir. Desteklenip desteklenmediği OPTIONS ve Allow ile öğrenilir, desteklemeyen sunucu 405 döndürür. Tarayıcı desteği için MDN uyumluluk tablosuna bakın.

Örnek: metotların davranışını küçük bir sunucuda görmek

Aşağıdaki kod, bellek içinde ürün tutan küçük bir istek işleyiciyi Request ve Response nesneleriyle kurar ve her metodu iki kez çağırır. Amaç idempotent olma kavramını somutlaştırmaktır: aynı PUT iki kez gönderildiğinde kaynağın son hali değişmez, sayaç artıran PATCH iki kez gönderildiğinde stok iki kez artar, POST iki kez gönderildiğinde iki ayrı kayıt oluşur. Gerçek bir sunucu değildir, ağ bağlantısı açmaz.

javascript
const kayitlar = new Map([['1', { ad: 'Kalem', stok: 10 }]]);
let sonId = 1;

async function sunucu(istek) {
  const id = new URL(istek.url).pathname.split('/')[2];
  switch (istek.method) {
    case 'GET':
      return Response.json(kayitlar.get(id) ?? null, { status: kayitlar.has(id) ? 200 : 404 });
    case 'PUT':
      kayitlar.set(id, await istek.json());
      return new Response(null, { status: 204 });
    case 'PATCH': {
      const { stokArtir } = await istek.json();
      kayitlar.get(id).stok += stokArtir;
      return new Response(null, { status: 204 });
    }
    case 'POST':
      kayitlar.set(String(++sonId), await istek.json());
      return new Response(null, { status: 201, headers: { Location: '/urunler/' + sonId } });
    case 'DELETE':
      kayitlar.delete(id);
      return new Response(null, { status: 204 });
    case 'OPTIONS':
      return new Response(null, { status: 204, headers: { Allow: 'GET, PUT, PATCH, POST, DELETE, OPTIONS' } });
    default:
      return new Response(null, { status: 405, headers: { Allow: 'GET, PUT, PATCH, POST, DELETE, OPTIONS' } });
  }
}

const gonder = (method, yol, veri) =>
  sunucu(new Request('https://example.com' + yol, {
    method,
    headers: veri ? { 'Content-Type': 'application/json' } : {},
    body: veri ? JSON.stringify(veri) : undefined,
  }));

for (let i = 0; i < 2; i++) await gonder('PUT', '/urunler/1', { ad: 'Kalem', stok: 50 });
console.log(JSON.stringify(kayitlar.get('1'))); // {"ad":"Kalem","stok":50}

for (let i = 0; i < 2; i++) await gonder('PATCH', '/urunler/1', { stokArtir: 5 });
console.log(JSON.stringify(kayitlar.get('1'))); // {"ad":"Kalem","stok":60}

for (let i = 0; i < 2; i++) await gonder('POST', '/urunler', { ad: 'Defter', stok: 3 });
console.log(kayitlar.size); // 3

const sil1 = await gonder('DELETE', '/urunler/2');
const sil2 = await gonder('DELETE', '/urunler/2');
console.log(sil1.status, sil2.status, kayitlar.size); // 204 204 2

const secenek = await gonder('OPTIONS', '/urunler/1');
console.log(secenek.status, secenek.headers.get('Allow'));
// 204 GET, PUT, PATCH, POST, DELETE, OPTIONS

Çıktıyı yorumlayalım. Stok 50 olarak iki kez PUT edildi ve sonuç 50 kaldı; { stokArtir: 5 } talimatıyla iki PATCH stoğu 60'a çıkardı, yani bu PATCH idempotent değil. İki POST iki yeni ürün ekledi ve kayıt sayısı 3 oldu; ağ kesintisinden sonra otomatik yeniden deneme yapan bir istemci burada çift kayıt yaratırdı. Aynı ürünü iki kez silmek kaynağın son durumunu değiştirmedi. Bu örnek ikinci silmede de 204 döndürüyor; gerçek bir API ikinci seferde 404 de döndürebilir ve bu idempotentliği bozmaz, çünkü ölçüt sunucudaki etkidir. OPTIONS ise desteklenen metotları Allow ile listeledi.

HTTP/2 ve HTTP/3 mesajları neyi değiştirir?

HTTP/1.x mesajları metin tabanlıdır; gövde sıkıştırılabilir ama başlıklar sıkıştırılamaz ve aynı bağlantıda tekrar tekrar gönderilir. Ayrıca istemcinin bir sonraki isteği göndermek için yanıtı beklemesi gereken satır başı engellenmesi (head-of-line blocking) sorunu vardır; paralel istek için birden fazla bağlantı açmak gerekir ve MDN bu sınırın tarayıcılarda tipik olarak 6 paralel bağlantı olduğunu belirtir. HTTP/2 mesajları ikili çerçevelere sarar ve tek TCP bağlantısı üzerinde numaralı akışlar (stream) halinde gönderir; başlık ve veri çerçeveleri ayrı işlendiği için başlıklar HPACK algoritmasıyla sıkıştırılabilir. Aynı bağlantıyla birden fazla isteği eşzamanlı yürütmeye çoğullama (multiplexing) denir.

HTTP/2'de başlangıç satırının yerini iki nokta üst üste ile başlayan sözde başlıklar alır: istekte :method, :scheme, :authority ve :path; yanıtta yalnızca :status. HTTP/3 ise TCP'deki satır başı engellenmesini aşmak için UDP üzerine kurulu QUIC protokolünü kullanır. Önemli olan, metotların, durum kodlarının ve başlıkların anlamının üç sürümde de aynı kalmasıdır; MDN tarayıcı ve sunucu HTTP/2 desteklediğinde bunun otomatik kullanıldığını ve geliştiricinin kullandığı API'lerde neredeyse hiçbir değişiklik gerekmediğini vurgular.

http
:method: GET
:path: /
:scheme: https
:authority: www.example.com
accept: */*

Sık yapılan hatalar

Sık yapılan hatalar: Durum değiştiren işlemleri GET ile yapmak., GET veya DELETE isteğine gövde koymak., PUT ile kısmi güncelleme göndermek., POST isteklerini otomatik yeniden...
  • Durum değiştiren işlemleri GET ile yapmak. Silme, sipariş verme veya ayar değiştirme bir GET bağlantısıyla yapılırsa başka siteler ve otomatik araçlar bu işlemi istemeden tetikleyebilir.
  • GET veya DELETE isteğine gövde koymak. Anlamı tanımsızdır, bazı sunucular reddeder ve fetch() gövdeli GET isteği kurarken hata fırlatır.
  • PUT ile kısmi güncelleme göndermek. PUT tam yerine koymadır; gönderilmeyen alanlar kaynağın yeni halinde de yer almaz. Kısmi değişiklik için PATCH kullanın.
  • POST isteklerini otomatik yeniden denemek. İdempotent olmadığı için çift kayıt riski vardır; yeniden denemeyi idempotent metotlarla sınırlayın ya da sunucuda tekrar korumasına dayanın.
  • Gerekçe ifadesine göre karar vermek. "OK" veya "Not Found" metni yalnızca bilgilendiricidir; kodda sayısal durumu kullanın.
  • TRACE ve CONNECT'i gereksiz yere açık bırakmak. CONNECT destekleyen proxy'leri bilinen portlarla sınırlayın.

Sık Sorulan Sorular

PUT ile POST arasındaki fark nedir?

PUT idempotenttir ve hedef URL'deki kaynağı istekteki içerikle oluşturur ya da tamamen değiştirir; aynı isteği tekrarlamak sonucu değiştirmez. POST idempotent değildir; aynı isteği tekrarlamak örneğin aynı siparişi birden fazla kez oluşturabilir.

OPTIONS isteklerini neden sunucu loglarında görüyorum?

Büyük olasılıkla bunlar tarayıcının CORS ön kontrol istekleridir. Basit istek koşullarını aşan, örneğin özel başlık ya da PUT kullanan bir çapraz köken isteğinden önce tarayıcı OPTIONS gönderir ve izin bekler.

HEAD isteği ne işe yarar?

Kaynağın gövdesini indirmeden, GET yanıtında gelecek başlıkları görmenizi sağlar. Dosya boyutunu Content-Length ile öğrenmek ya da önbellekteki kopyanın güncel olup olmadığını anlamak için kullanılır.

HTTP/2 kullanmak için kodumu değiştirmem gerekir mi?

MDN'e göre neredeyse hiç gerekmez. Tarayıcı ve sunucu HTTP/2 desteklediğinde otomatik olarak kullanılır; metotlar, başlıklar ve durum kodları aynı anlamı taşır.

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 · HTTP durum kodları · Cache-Control ve koşullu istekler · Fetch API kullanımı · Request, Response ve Headers · CORS nedir · CSRF saldırısı ve korunma