İçeriğe geç
academia.sh

Ders 10 / 34

HTTP Yöntemlerinin Doğru Kullanımı

Yöntemin taşıdığı sözler: güvenli yöntemin veriyi değiştirmemesi ve etkisiz yöntemin tekrarlandığında tek etki üretmesi; her özelliğin veritabanındaki satır sayısı sayılarak sınanması, PATCH'in artırım ve atama yüzleri, PUT ile POST arasındaki kimlik seçimi.

İçindekiler

Bir önceki ders yolu kaynağın kimliğine ayırdı ve “ne yapılacağını yöntem söyler” diyerek geçti. O cümle bir görev dağılımından fazlasını içerir. HTTP yöntemi yalnızca sunucuya gönderilen bir emir değildir; istemcinin, önbelleklerin ve aradaki her bileşenin isteğe bakarak güvendiği bir sözdür.

Bu ders o sözleri tanımlar ve her birini ölçer. Ölçüm biçimi tektir: aynı istek arka arkaya iki kez gönderilir ve veritabanındaki satır sayısı ile alan değerleri her adımda sayılır. Bir yöntemin sözünü tuttuğu ya da tutmadığı, tartışmayla değil sayılarla belirlenir.

Yöntem Bir Ad Değil, Bir Söz

İnternet Nasıl Çalışır kursu iki özelliği tanıtmıştı. Güvenli (safe) bir yöntem sunucu tarafında gözlenebilir bir değişiklik bırakmaz; okuma yapar, yazma yapmaz. Etkisiz (idempotent) bir yöntem, aynı isteğin bir kez ya da arka arkaya beş kez gönderilmesi arasında sunucunun son durumu bakımından fark bırakmaz. İkisi bağımsızdır: her güvenli yöntem etkisizdir, ama her etkisiz yöntem güvenli değildir.

Yöntem Güvenli Etkisiz Gövde taşır
GET evet evet hayır
HEAD evet evet hayır
OPTIONS evet evet hayır
POST hayır hayır evet
PUT hayır evet evet
PATCH hayır tasarıma bağlı evet
DELETE hayır evet genellikle hayır

Bu özellikler sunucunun iç işi değildir; dışarıya verilmiş taahhütlerdir. Bir önbellek, GET yanıtını saklayıp bir sonraki isteğe sunucuya hiç gitmeden yanıt verebilir — çünkü GET’in bir yan etkisi olmadığına güvenir. Bir istemci, ağ hatası aldığında PUT isteğini yeniden gönderebilir — çünkü tekrarın zararsız olduğuna güvenir. Uygulama Mimarisi kursundaki istek katmanı bu güveni yeniden denenebilirlik olarak kodlamıştı.

Taahhüt tutulmazsa kırılan şey sunucu değil, sunucuya güvenen taraflardır.

Güvenlilik: Okuma Veriyi Değiştirmez

Aşağıdaki servis, ölçüm için bilerek bir kusur taşır: ödünç iadesi hem doğru yoldan hem de okuma görünümlü bir adresten yapılabilir.

// yontem-sunucusu.mjs — yontem ozelliklerini olcmek icin kurulmus servis
import { createServer } from "node:http";
import { DatabaseSync } from "node:sqlite";

const db = new DatabaseSync("kutuphane.db");
const govdeOku = (istek) => new Promise((coz) => {
  let veri = ""; istek.on("data", (p) => (veri += p));
  istek.on("end", () => coz(veri ? JSON.parse(veri) : {}));
});
const yanitla = (yanit, kod, nesne) => {
  yanit.writeHead(kod, { "content-type": "application/json; charset=utf-8" });
  yanit.end(nesne === null ? "" : JSON.stringify(nesne));
};

const sunucu = createServer(async (istek, yanit) => {
  const yol = new URL(istek.url, "http://127.0.0.1").pathname;
  const y = istek.method;
  const kitap = /^\/kitaplar\/([^/]+)$/.exec(yol);
  const odunc = /^\/oduncler\/(\d+)$/.exec(yol);
  const iadeYolu = /^\/oduncler\/(\d+)\/iade$/.exec(yol);

  if (y === "GET" && yol === "/oduncler")
    return yanitla(yanit, 200, db.prepare("SELECT * FROM odunc").all());

  // YANLIS: okuma gorunumlu bir adres veriyi degistiriyor.
  if (y === "GET" && iadeYolu) {
    db.prepare("UPDATE odunc SET iade = '2026-03-12' WHERE id = ? AND iade IS NULL")
      .run(Number(iadeYolu[1]));
    return yanitla(yanit, 200, { sonuc: "iade alindi" });
  }

  if (y === "POST" && yol === "/oduncler") {
    const g = await govdeOku(istek);
    const s = db.prepare("INSERT INTO odunc (uye, isbn, verilis, iade) VALUES (?,?,?,NULL)")
                .run(g.uye, g.isbn, g.verilis);
    return yanitla(yanit, 201, { id: Number(s.lastInsertRowid) });
  }

  if (y === "PUT" && kitap) {
    const g = await govdeOku(istek);
    // Tam degisim: gonderilen gosterim kaydin tamamini belirler.
    const varMi = db.prepare("SELECT 1 FROM kitap WHERE isbn = ?").get(kitap[1]);
    db.prepare(`INSERT INTO kitap (isbn, ad, yazar, yil, sube, kopya) VALUES (?,?,?,?,?,?)
                ON CONFLICT(isbn) DO UPDATE SET
                  ad=excluded.ad, yazar=excluded.yazar, yil=excluded.yil,
                  sube=excluded.sube, kopya=excluded.kopya`)
      .run(kitap[1], g.ad, g.yazar, g.yil, g.sube, g.kopya);
    return yanitla(yanit, varMi ? 200 : 201, db.prepare("SELECT * FROM kitap WHERE isbn = ?").get(kitap[1]));
  }

  if (y === "PATCH" && kitap) {
    const g = await govdeOku(istek);
    if ("kopyaEkle" in g)  // artirim: onceki degere baglidir
      db.prepare("UPDATE kitap SET kopya = kopya + ? WHERE isbn = ?").run(g.kopyaEkle, kitap[1]);
    if ("kopya" in g)      // atama: onceki degerden bagimsizdir
      db.prepare("UPDATE kitap SET kopya = ? WHERE isbn = ?").run(g.kopya, kitap[1]);
    return yanitla(yanit, 200, db.prepare("SELECT isbn, kopya FROM kitap WHERE isbn = ?").get(kitap[1]));
  }

  if (y === "DELETE" && odunc) {
    db.prepare("DELETE FROM odunc WHERE id = ?").run(Number(odunc[1]));
    return yanitla(yanit, 204, null);
  }

  yanitla(yanit, 404, { hata: "yol_yok" });
});

sunucu.listen(8476, "127.0.0.1", () => console.log("yontem sunucusu 127.0.0.1:8476"));

Şimdi bu servise, sayfadaki bağlantıları sırayla izleyen bir istemci gelsin. Böyle bir istemci yalnızca GET yollar; bağlantının arkasında ne olduğunu bilmez.

# Okuma gorunumlu adresi bir baglanti tarayicisi izlerse ne olur?
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
sqlite3 kutuphane.db "ALTER TABLE kitap ADD COLUMN kopya INTEGER NOT NULL DEFAULT 1;"
node yontem-sunucusu.mjs & sunucu=$!
sleep 0.4

acik() { sqlite3 kutuphane.db "SELECT COUNT(*) FROM odunc WHERE iade IS NULL;"; }
printf 'baslangicta acik odunc      : %s\n' "$(acik)"

# Sayfadaki butun baglantilari izleyen bir istemci: yalnizca GET yollar.
for yol in /oduncler /oduncler/1/iade /oduncler/3/iade; do
  printf 'GET %-22s -> %s\n' "$yol" "$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:8476$yol")"
done
printf 'GET sonrasi acik odunc      : %s\n' "$(acik)"

kill $sunucu
yontem sunucusu 127.0.0.1:8476
baslangicta acik odunc      : 2
GET /oduncler              -> 200
GET /oduncler/1/iade       -> 200
GET /oduncler/3/iade       -> 200
GET sonrasi acik odunc      : 0

İki açık ödünç kaydı, hiçbir kullanıcı iade işlemi yapmadan kapandı. İstemci kural çiğnemedi: bağlantı izleyen bir program için GET, sorulması serbest bir sorudur. Kuralı çiğneyen sunucudur.

Bu kusurun ölçeği tasarım hatasının ötesine geçer. Sayfa önizlemesi üreten bir araç, bağlantı denetleyen bir betik, adres çubuğuna yazılanı önceden getiren bir tarayıcı — hepsi aynı varsayımla çalışır. Şema iki tabloyla kurulmuş bir örnekte iki satır kaybolur; gerçek bir katalogda kayıp, izlenen bağlantı sayısı kadardır.

Doğru tasarımda /oduncler/{id}/iade gibi bir adres hiç açılmaz; iade, bir önceki derste gösterildiği gibi PATCH /oduncler/{id} isteğiyle yapılır. Adres okuma görünümlüyse yöntem de okuma olmalıdır ve tersi.

Etkisizlik: Aynı İsteği İki Kez Göndermek

Etkisizlik ölçümü doğrudandır. Her yöntem arka arkaya iki kez çağrılır; iki çağrının sonucu bir çağrınınkinden farklıysa yöntem etkisiz değildir.

# Her yontem iki kez cagrilir; her cagridan sonra veritabanindaki etki sayilir.
rm -f kutuphane.db && sqlite3 kutuphane.db < sema.sql
sqlite3 kutuphane.db "ALTER TABLE kitap ADD COLUMN kopya INTEGER NOT NULL DEFAULT 1;"
node yontem-sunucusu.mjs & sunucu=$!
sleep 0.4

T=http://127.0.0.1:8476
say() { printf '%-26s odunc=%s  kopya=%s\n' "$1" \
  "$(sqlite3 kutuphane.db 'SELECT COUNT(*) FROM odunc;')" \
  "$(sqlite3 kutuphane.db "SELECT kopya FROM kitap WHERE isbn='978-0131103627';")"; }

say "baslangic"
curl -sS -o /dev/null $T/oduncler; curl -sS -o /dev/null $T/oduncler
say "GET x2"
for i in 1 2; do curl -sS -o /dev/null -X POST -H 'content-type: application/json' \
  -d '{"uye":"U-1002","isbn":"978-0131103627","verilis":"2026-03-10"}' $T/oduncler; done
say "POST x2"
for i in 1 2; do curl -sS -o /dev/null -X PUT -H 'content-type: application/json' \
  -d '{"ad":"C Programlama Dili","yazar":"Ritchie","yil":1988,"sube":"S-01","kopya":4}' \
  $T/kitaplar/978-0131103627; done
say "PUT x2"
for i in 1 2; do curl -sS -o /dev/null -X PATCH -H 'content-type: application/json' \
  -d '{"kopyaEkle":2}' $T/kitaplar/978-0131103627; done
say "PATCH artirim x2"
for i in 1 2; do curl -sS -o /dev/null -X PATCH -H 'content-type: application/json' \
  -d '{"kopya":6}' $T/kitaplar/978-0131103627; done
say "PATCH atama x2"
printf '%-26s %s %s\n' "DELETE x2 (durum kodu)" \
  "$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE $T/oduncler/2)" \
  "$(curl -sS -o /dev/null -w '%{http_code}' -X DELETE $T/oduncler/2)"
say "DELETE x2"

kill $sunucu
yontem sunucusu 127.0.0.1:8476
baslangic                  odunc=3  kopya=1
GET x2                     odunc=3  kopya=1
POST x2                    odunc=5  kopya=1
PUT x2                     odunc=5  kopya=4
PATCH artirim x2           odunc=5  kopya=8
PATCH atama x2             odunc=5  kopya=6
DELETE x2 (durum kodu)     204 204
DELETE x2                  odunc=4  kopya=6

Satırlar tek tek okunmalıdır.

GET x2 hiçbir sayıyı değiştirmedi: yöntem güvenlidir, dolayısıyla etkisizdir.

POST x2 ödünç sayısını üçten beşe çıkardı. İki istek, iki kayıt üretti. POST’un tanımı budur: koleksiyona yeni bir üye eklenir ve kimliği sunucu belirler. Ağ koptuğu için yeniden gönderilen bir POST, ikinci bir ödünç kaydı doğurur. Bu, tasarımın bozukluğu değil, POST’un doğasıdır; çözümü dokuzuncu dersin konusudur.

PUT x2 kopya sayısını 4 yaptı ve orada bıraktı. Gönderilen gövde kaydın tamamını belirlediği için ikinci istek birinciyle aynı son durumu üretir. Kaç kez gönderildiği sonucu değiştirmez.

PATCH artırım x2 kopya sayısını 4’ten 8’e çıkardı. Gövde kopyaEkle alanını taşıyor, yani yeni değer eskisine bağlı. Bu bir PATCH kusuru değildir; PATCH’in etkisizliğinin gövdeye bağlı olduğunun kanıtıdır. PATCH atama x2 aynı yöntemle 6 değerini üretti ve ikinci istek onu değiştirmedi.

Buradan çıkan tasarım kuralı nettir: kısmi güncelleme gövdesi artırım değil atama üzerine kurulursa yöntem etkisiz olur. Artırım gerçekten gerekiyorsa — sayaç, bakiye, stok — etkisizlik gövdeyle sağlanamaz; dokuzuncu dersteki anahtar düzeneği gerekir.

DELETE x2 iki isteğe de 204 döndürdü ve ödünç sayısı beşten dörde indi, ikinci silmede düşmedi. Etkisizliğin tanımı budur: son durum aynıdır. İkinci isteğe 404 döndürmek de savunulabilir bir seçimdir ve etkisizliği bozmaz, çünkü etkisizlik yanıt kodunun değil sunucu durumunun özelliğidir. Hangisinin seçileceği bir sonraki dersin konusudur.

PUT ile POST Arasındaki Seçim

İkisi de yazma yapar; ayrımı yapan şey kimliği kimin belirlediğidir.

POST, kimliği sunucunun belirlediği durumda kullanılır. İstemci /oduncler koleksiyonuna bir gövde yollar, sunucu yeni kaydın kimliğini üretir ve yanıtta bildirir. İstemci isteği göndermeden önce hedef adresi bilemez.

PUT, kimliği istemcinin bildiği durumda kullanılır. /kitaplar/978-0131103627 adresine yapılan PUT, o adreste bir kaynak yoksa oluşturur, varsa gönderilen gösterimle değiştirir. Yukarıdaki sunucu bu ayrımı durum koduna da yansıtır: kayıt yoksa 201, varsa 200.

PUT’un ikinci özelliği çoğu zaman gözden kaçar: gönderilmeyen alanlar boşalır. PUT bir tam değişimdir, kısmi güncelleme değildir. Gövdede yazar alanı yoksa kaydın yazarı silinmiş sayılır. Bu davranış istenmiyorsa doğru yöntem PATCH’tir. Bir servisin PUT’u kısmi güncelleme gibi işletmesi, istemcinin etkisizlik varsayımını bozmaz ama sözleşmeyi belirsizleştirir: aynı gövde iki serviste iki farklı sonuç verir.

Yöntem Uyuşmazlığı

Bir adres var olduğu hâlde istenen yöntemi desteklemiyorsa — örneğin /uyeler/U-1001/durum kaynağına DELETE gelirse — yanıt “bulunamadı” olmamalıdır. Kaynak duruyor, yöntem uygulanamıyor. HTTP bunun için ayrı bir durum kodu tanımlar ve yanıtın hangi yöntemlerin kabul edildiğini bildirmesini bekler. Yukarıdaki sunucu bu ayrımı yapmıyor; eşleşmeyen her şeye 404 dönüyor. Bir sonraki ders bu eksiği ölçüp kapatır.

Özet

  • Yöntem, sunucunun dışarıya verdiği bir taahhüttür: güvenli yöntem gözlenebilir değişiklik bırakmaz, etkisiz yöntem tekrarlandığında son durumu değiştirmez.
  • Okuma görünümlü bir adresin veri değiştirmesi, bağlantı izleyen her programı silici hâline getirir; ölçümde üç GET isteği iki açık ödünç kaydını kapattı.
  • POST etkisiz değildir ve olmaya da çalışmaz; kimliği sunucu ürettiği için her istek yeni kayıt doğurur.
  • PUT tam değişimdir ve etkisizdir; gönderilmeyen alanlar boşalır, bu yüzden kısmi güncelleme için uygun değildir.
  • PATCH’in etkisizliği gövdeye bağlıdır: atama biçimindeki gövde etkisiz, artırım biçimindeki gövde değildir — ölçümde kopya sayısı 4’ten 8’e çıktı.
  • DELETE etkisizdir; ikinci silme isteğinin farklı durum kodu döndürmesi bu özelliği bozmaz, çünkü etkisizlik sunucu durumunun özelliğidir.

Sonraki Adım

Bu derste yanıtların durum kodları büyük ölçüde varsayıldı: oluşturmada 201, silmede 204, eşleşmeyen her şeyde 404. Oysa bunların her biri bir seçimdir ve seçim yanlış yapıldığında istemci hatayı fark etmez. Bir kaynağın var olmaması ile ona erişim izni olmaması aynı kod mu döner? İş kuralının reddettiği bir istek ile gövdesi bozuk bir istek nasıl ayrılır? Sonraki ders aynı senaryo kümesini iki farklı sunucuya sorar, ürettikleri kodları yan yana koyar ve yanlış eşlemenin istemcide ne ürettiğini gösterir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat