Go HTTP sunucusunda panic'i recovery middleware ile nasıl yönetirim?
Soru
Go ile yazılı HTTP API'mizde nil pointer dereference yüzünden beklenmedik bir panic oluştu. Bu panic tüm sürecin aniden kapanmasına ve o an içeride olan tüm HTTP isteklerinin drop edilmesine sebep oldu. Go'nun `recover()` mekanizmasıyla panic'leri yakalayan, loglayan ve istemciye zarif bir 500 dönen merkezi bir middleware'i nasıl kurarım?
Cevap
Kısa cevap: net/http bir handler’daki panic’i zaten kendisi recover eder; yani tek bir kötü istek süreci öldürmez. Ama istemciye düzgün bir yanıt da gitmez: bağlantı kopar ve stack trace yalnızca sunucunun hata loguna yazılır; varsayılanda bu stderr’dir. Recovery middleware’i kontrollü bir 500, request id’li yapılandırılmış log ve bir metrik için gerekir — ve http.ErrAbortHandler’ı yeniden fırlatmalıdır.
Kısa cevap
Asıl mesele şu: sunucunun kendi recover’ı bir güvenlik ağıdır, hata yönetimi değil. Handler’daki bir nil deref, binlerce sağlıklı isteği sizinle birlikte öldürmez; ama panic eden isteğin sahibi cevap yerine kopmuş bir bağlantı görür ve geriye yalnızca stderr’de ham bir stack trace kalır. Süreciniz gerçekten kapandıysa o güvenlik ağının dışında bir panic arayın: go ile açılmış bir goroutine’de kimsenin recover etmediği panic programı sonlandırır. Servisi standart kütüphaneyle kurmanın detayını Go’da HTTP servisi yazmak yazısında anlatmıştım.
Neden
-
Yerleşik recover ne yanıt verir ne sinyal. Panic eden bir handler için sunucu panic’i recover eder, stack trace’i hata loguna yazar ve ağ bağlantısını kapatır ya da HTTP/2
RST_STREAMgönderir. İstemci500görmez, log satırında request id yoktur, hiçbir metrik artmaz. -
Recover yalnızca kendi goroutine’ini yakalar. Sunucunun recover’ı handler’ı çalıştıran goroutine’i kapsar. Handler içinde
go func(){...}()açtıysanız, oradaki panic ne sunucunun ne de middleware’inizin recover’ına takılır — ve bütün süreci sonlandırır; içerideki tüm istekleri drop eden durum budur. -
Sessizce yutulan panic, recover edilmemesi kadar tehlikelidir. Metrik yoksa aynı bug günlerce gizli kalır.
Ne yapmalı
-
Her isteği
defer recover()ile sarın. Middleware içindedefer func(){ if r := recover(); r != nil { /* logla + 500 yaz */ } }()kurun. -
Bu middleware’i zincirin en dışına koyun. Recovery en dıştaki katman olmalı ki kendisinden sonra gelen tüm handler ve middleware’lerin panic’lerini sarsın.
-
Stack trace’i + request id’yi loglayın, ama içini sızdırmayın.
debug.Stack()ile stack trace’i ve isteğin id’sini loglayın; istemciye generic bir500dönün. -
http.ErrAbortHandler’ı yeniden fırlatın. Yakalanan değerhttp.ErrAbortHandlerise500yazmak yerine aynı değeri yeniden fırlatın. Handler’ların yanıtı bilerek yarıda kesmek için kullandığı işaret değeri budur; yeniden fırlatmak sunucunun bu değere uyguladığı davranışı korur: istemci yarım kalmış bir yanıt görür, stack trace loglanmaz. -
Panic’i görünür kılın — bir metrik basın. Her recover’da bir sayaç artırın.
-
Açtığınız her goroutine’e kendi recover’ını verin. Aksi hâlde süreç yine ölür.
Sonuç: Ben olsam kontrollü bir 500 dönen, request id ile loglayan, panic’i sayan ve http.ErrAbortHandler’ı yeniden fırlatan bir istek başı recover middleware’i + her goroutine için ayrı recover kurardım. İki uyarı: recover, beklenmedik bug’lar (nil deref) içindir, hata dönmenin yerine geçmez — beklenen hataları error ile dönün. Ve buna rağmen süreci yeniden başlatan bir supervisor tutun.
İlgili Yazılar
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.