Olayı gönderdik ama müşteri almadı: kendi webhook sisteminizi nasıl tasarlarsınız?
Entegratörünüz "sipariş oluştuğunda bize anlık bildirin" dediğinde iş bir URL'ye POST atmakla bitmiyor. Dayanıklı bir webhook sistemi dört ayrı parçadan oluşur: olayın uygulama içinde güvenilir şekilde üretilmesi (veri değişikliğiyle aynı işlemde, kuyruğa yazılarak), teslim katmanı (zaman aşımı, üstel geri çekilme, ölü mektup kutusu), doğrulanabilirlik (gövdeyi kapsayan imza, zaman damgası, tekrar koruması) ve gözlemlenebilirlik (her deneme için teslimat kaydı ve elle tekrar gönderim). Dördü birden kurulmazsa tartışma hep aynı yerde biter: müşteri "bildirim gelmedi" der, sizde ise gelip gelmediğini gösteren bir ekran yoktur.
Önce karar: webhook mu, yoklama mı, ikisi birden mi?
Webhook gecikmeyi düşürür, yoklama (polling) bütünlüğü garantiler. Bunlar birbirinin alternatifi değil, birbirinin tamamlayıcısı. Shopify kendi geliştirici dokümanında bunu açıkça yazıyor: webhook teslimatı her zaman garanti değildir, bu yüzden uygulamalar updated_at filtresiyle veri çeken bir mutabakat işi çalıştırmalıdır. Stripe de aynı boşluğu olay listesi uç noktasıyla kapatıyor.
Pratik kural şu: müşteriye hem webhook hem de "şu tarihten beri değişenleri ver" diyebileceği bir uç nokta verin. Webhook kaçtığında müşterinin sizi aramadan kendi kendini onarabileceği bir yol kalsın. Tarayıcıda açık duran ekranı canlı güncellemek bambaşka bir problem, orada WebSocket ve SSE tartışması devreye girer (canlı güncelleme yöntemleri).
Olayı nerede üretiyorsunuz?
En sık görülen hata, HTTP isteğinin içinden webhook göndermek. İki şekilde patlıyor. Birincisi, işlem (transaction) geri alındığında olay çoktan gitmiş olur ve müşteri sizde var olmayan bir siparişi işler. İkincisi, alıcı yavaşsa kullanıcının isteği de yavaşlar; alıcı çökmüşse kullanıcının isteği de çöker.
Doğrusu giden kutusu (outbox) kalıbı: durum değişikliğiyle aynı veritabanı işleminde bir olay satırı yazarsınız, ayrı bir işçi bu satırları okuyup teslim eder. Böylece olay ancak veri gerçekten değiştiyse üretilir, kuyruk o an erişilemez olsa bile kaybolmaz ve üretim sırası veritabanında korunur. Kuyruk, işçi havuzu ve yeniden deneme ayarlarını arka plan işleri yazısında ayrıntılı anlattık.
İnce yük mü, tam anlık görüntü mü?
İki seçenek var. İnce yük (thin payload) olayın kimliğini, tipini, zamanını ve etkilenen kaydın kimliğini taşır; alıcı güncel veriyi sizin API'nizden çeker. Tam anlık görüntü (snapshot) ise kaydın o andaki tüm alanlarını gövdeye koyar.
Stripe yeni entegrasyonlar için ince yükü öneriyor ve alıcının ilgili nesneyi API'den çekmesini bekliyor. Standard Webhooks spesifikasyonu da yükün yaklaşık 20 KB'ın altında tutulmasını, büyük verinin gövdeye konmayıp bağlantı olarak verilmesini söylüyor. İnce yükün üç avantajı var: alıcı her zaman güncel veriyi görür, hassas alanlar gereksiz yere ağda dolaşmaz, sıra karışıklığının etkisi azalır. Bedeli de şu: alıcı her olay için bir API çağrısı daha yapar, yani webhook trafiğinizin yarattığı yükü API tarafında da planlamanız gerekir (hız sınırlama ve kota).
Hangisini seçerseniz seçin, gövdede dört alan mutlaka bulunsun: benzersiz olay kimliği, olay tipi, olayın gerçekleştiği an ve bir şema sürüm işareti. Sürüm işareti olmadan ilk alan değişikliğinde tüm entegratörleri kırarsınız (API sürümleme ve geriye dönük uyumluluk).
İmza: başlıkta, ham gövdeyi kapsayan
Bir webhook uç noktası internete açık ve kimlik doğrulaması olmayan bir kapıdır. Gövdenin sizden geldiğini alıcının kanıtlayabilmesi gerekir ve bunun yolu paylaşılan bir sırla üretilen HMAC imzasıdır. Sektörde üç yaygın kurulum var ve üçü de aynı fikrin varyasyonu:
- Standard Webhooks:
webhook-id,webhook-timestampvewebhook-signaturebaşlıkları. İmzalanan metinid.timestamp.gövdebiçiminde birleştirilir, HMAC-SHA256 ile imzalanır, sırwhsec_önekiyle saklanır. Spesifikasyon ayrıca ed25519 ile asimetrik imzayı da tanımlıyor; o zaman alıcının elinde sizin sırrınız değil yalnızca açık anahtar bulunur. - Stripe:
Stripe-Signaturebaşlığı içindet=zaman damgası vev1=imzası. İmzalanan metin zaman damgası, nokta ve ham gövdedir. - GitHub:
X-Hub-Signature-256başlığı,sha256=önekiyle onaltılık HMAC-SHA256. Dokümantasyon karşılaştırma için düz==kullanılmamasını, sabit zamanlı karşılaştırma fonksiyonu çağrılmasını açıkça söylüyor.
Üç pratik kural. Bir: imza ham gövde üzerinden hesaplanır, JSON'u ayrıştırıp yeniden seri hale getiren bir ara katman imzayı bozar. Stripe'ın dokümanındaki en sık doğrulama hatası tam olarak budur. İki: zaman damgası imzalanan metnin içinde olsun, böylece alıcı eski bir isteğin tekrar gönderilmesini (replay) fark edebilir. Stripe kütüphanelerinin varsayılan toleransı 5 dakika, Slack da aynı pencereyi kullanıyor ve beş dakikadan eski zaman damgalarının reddedilmesini söylüyor. Üç: sır döndürmeyi baştan planlayın. Stripe bir uç nokta için eski sırrı 24 saate kadar canlı tutup her iki sırla ayrı imza üretiyor, böylece alıcı kodunu güncellerken kesinti yaşanmıyor.
Sırrı asla URL'ye koymayın. GitHub bunu ayrıca uyarıyor, çünkü URL'ler erişim kayıtlarına, hata izlerine ve ekran görüntülerine düşer (sır yönetimi).
En az bir kez teslim, sıra garantisi yok
Bu iki cümleyi entegratör dokümanınıza yazmazsanız, karşı taraf onları üretimde keşfeder. Stripe olayların üretildiği sırada teslim edileceğini garanti etmiyor ve created alanının ne sıralama ne de tekrar tespiti için kullanılmasını istiyor; bunun yerine olay kimliğini saklamanızı söylüyor. Shopify daha da nettir: ne aynı konu içinde ne de aynı kaynağın farklı konuları arasında sıra garantisi vardır, products/update olayı products/create olayından önce gelebilir.
Bu yüzden alıcıya üç şey vermeniz gerekir. Benzersiz ve yeniden denemelerde değişmeyen bir olay kimliği (Stripe olay kimliği, GitHub'da X-GitHub-Delivery, Shopify'da X-Shopify-Webhook-Id bunu yapar), kaydın sürümünü ya da son güncellenme anını taşıyan bir alan, ve dokümanda tek cümlelik bir talimat: aynı kimliği ikinci kez görürseniz işlemeyin, elinizdekinden eski bir sürüm geldiyse atın. Mükerrer işlem riskinin faturaya nasıl yansıdığını ödeme entegrasyonu yazısında örneklemiştik.
Yeniden deneme takvimi ve durum kodu politikası
Yeniden deneme üstel geri çekilmeyle ve rastgelelik (jitter) eklenerek yapılır. Rastgelelik olmazsa bir kesinti sonrası tüm bekleyen olaylar aynı saniyede geri döner ve alıcıyı siz yıkarsınız (dış servis kesintisine dayanıklılık).
Süreler konusunda referans alabileceğiniz üç örnek: Stripe canlı modda teslimatı üç güne kadar deniyor, Standard Webhooks spesifikasyonundaki örnek takvim ilk denemeden yaklaşık 75,5 saat sonra bitiyor, Slack ise olay aboneliklerinde üç deneme yapıyor (hemen, yaklaşık bir dakika sonra, yaklaşık beş dakika sonra). Hangi aralığı seçeceğiniz ürününüze bağlı, ama takvim yazılı olmalı ve entegratör ne kadar süre korunduğunu bilmeli.
Durum kodlarına verilen tepki de tasarımın parçası. Standard Webhooks'un önerdiği politika sade: 2xx başarı, 3xx başarısızlık sayılır ve yönlendirme takip edilmez (Stripe da yönlendirmeyi başarısızlık kabul ediyor), 410 uç noktayı kapatmak için bir işarettir, 429, 502 ve 504 ise hızı düşürmek gerektiğini söyler. Retry-After başlığı geldiyse ona uyun.
Uzun süre başarısız olan uç noktayı kapatma kuralınız da olsun, ama kapatmadan önce müşteriye haber verin. Slack bu konuda ölçülü bir örnek: 60 dakika içinde denemelerin %95'inden fazlası başarısız olursa abonelik geçici olarak kapatılıyor ve uygulama sahibine e-posta gidiyor, saatte 1.000'den az olay alan uygulamalar ise bu otomatik kapatmanın dışında tutuluyor.
Zaman bütçesi ve tek yavaş alıcının tıkadığı kuyruk
Alıcıdan ne kadar beklediğinizi yazın, çünkü bu sayı entegratörün mimarisini belirler. GitHub 10 saniye içinde 2xx bekliyor, Slack üç saniye. Stripe ise işin özünü tek cümleyle söylüyor: ağır işe girişmeden önce 2xx dönün, muhasebe sistemine faturayı işlemeden önce 200 yanıtı verin.
Gönderen tarafta aynı disiplin size de lazım. Zaman aşımınız birkaç saniyeyi geçmesin ve her hedef için ayrı kuyruk ya da en azından ayrı eşzamanlılık bütçesi tutun. Tek bir kuyrukta ilerleyen tasarımda çöken bir müşteri uç noktası, kuyruğun başını tıkar ve diğer tüm müşterilerin olayları onun arkasında bekler. Sürekli hata veren hedefi devre kesiciyle bir süre devre dışı bırakmak, aynı hızda denemeye devam etmekten hem sizin hem de alıcının lehinedir.
Müşterinin verdiği URL bir saldırı yüzeyidir
Webhook hedefini müşteri giriyorsa sunucunuza istediği adrese istek attırabiliyor demektir. Bu, sunucu tarafı istek sahteciliğinin (SSRF) ders kitabı örneğidir: hedef olarak iç ağdaki bir yönetim paneli, veritabanı arayüzü ya da bulut sağlayıcısının 169.254.169.254 üzerindeki örnek üstverisi verilebilir. Standard Webhooks spesifikasyonu güvenlik maddesinde bunu ilk sıraya koyuyor ve iki somut önlem öneriyor: giden webhook trafiğini iç IP aralıklarını engelleyen filtreli bir vekil sunucudan geçirin (örnek olarak Smokescreen'i veriyor), webhook işçilerini iç servislere erişimi olmayan özel bir alt ağda çalıştırın.
Buna ekleyeceğiniz dört kural var: yalnızca HTTPS kabul edin, yönlendirmeleri takip etmeyin, alan adını çözümledikten sonra bağlantı anında IP'yi doğrulayın (DNS yeniden bağlama saldırısı tam bu boşluğu kullanır) ve yanıt gövdesinin okunacak boyutunu sınırlayın. Uygulama katmanındaki benzer kapıların listesi için API güvenliği yazımıza bakabilirsiniz.
"Bildirim gelmedi" dendiğinde bakacağınız ekran
Bir webhook sistemini bakılabilir yapan şey teslimat kaydıdır. Her deneme için şunlar dursun: olay kimliği, hedef, deneme numarası, HTTP durum kodu, yanıt gövdesinin kısaltılmış hali, süre ve varsa bir sonraki deneme zamanı. Stripe'ın panelinde her olayın durumu Delivered, Pending veya Failed olarak görünüyor ve başarısız denemelerde bağlantı hatası, TLS hatası ve zaman aşımı ayrı ayrı adlandırılıyor. Bu ayrım önemli, çünkü 404 ile TLS hatası iki farklı ekibe gidecek iki farklı sorundur.
Elle tekrar gönderimi de baştan planlayın ve süresini açıkça yazın. Stripe panelden tekrar göndermeye olayın oluşmasından sonra 15 gün, komut satırından 30 gün izin veriyor ve elle gönderimin otomatik yeniden denemeleri iptal etmediğini ayrıca uyarıyor. Bu ekranı müşteriye de açarsanız destek kuyruğunuzdaki "geldi mi gelmedi mi" trafiğinin büyük kısmı kaybolur.
Son olarak uyarıyı hedef bazında kurun. Toplam başarı oranınız %99 iken tek bir müşterinin uç noktası iki gündür tamamen kapalı olabilir ve bu genel grafikte görünmez. Eşik ve uyarı tasarımını SLO ve hata bütçesi yazısında anlattık.
Bu haftaya sığan üç adım
Bir: tek bir olay tipini uçtan uca gözden geçirin. Olay nerede üretiliyor (işlemin içinde mi, sonrasında mı), gövdede olay kimliği ve sürüm işareti var mı, imza ham gövdeyi mi kapsıyor.
İki: teslimat kaydınız yoksa ilk işiniz o olsun. Kayıt yoksa her tartışma "bizde gönderildi görünüyor" ile "bize ulaşmadı" arasında sıkışır ve kimse ilerleyemez.
Üç: entegratör dokümanına dört cümle ekleyin: teslimat en az bir kezdir, sıra garanti edilmez, 2xx yanıtını işe girişmeden önce dönün, imzayı ham gövde üzerinden doğrulayın. Yanına zaman aşımı sürenizi ve yeniden deneme takvimini yazın.
Wedevit olarak webhook katmanını giden kutusu kalıbıyla kuruyor, imza ve sır döndürme şemasını standartlara uygun tasarlıyor, hedef bazında kuyruk ve devre kesici ayırıyor, teslimat kaydı ve tekrar gönderim ekranını entegratörün kendi kendine kullanabileceği hale getiriyoruz. Somut ilk adım: bugün bir müşteri "dün akşamki siparişin bildirimi gelmedi" dese, hangi ekrandan kaç deneme yapıldığını ve her denemede ne döndüğünü gösterebilir misiniz?
Bu konuda yardıma mı ihtiyacınız var?