Request timing va error handler middleware
Bu darsda API’ni barqaror qiladigan uchta middleware yozamiz:
- Timing — har bir so‘rov qancha vaqt olganini o‘lchaydi va sekin so‘rovlarni aniqlaydi.
- Recoverer — handler ichidagi
panicni ushlaydi va serverning "yiqilishi"ga yo‘l qo‘ymaydi. - Error handler — handlerlar qaytargan xatolarni bitta joyda, bir xil formatda qayta ishlaydi.
Oldingi darsda yozilgan logging middleware bilan birga ular har qanday API uchun mustahkam poydevor bo‘ladi.
1. Timing middleware
Foydalanuvchi "sayt sekin ishlayapti" desa, qaysi endpoint sekin ekanini bilish kerak. Timing middleware har bir so‘rovning davomiyligini o‘lchaydi va natijani ikki joyga yozadi:
- javobdagi
Server-Timingsarlavhasiga. Uni brauzerning DevTools oynasida ko‘rish mumkin; - belgilangan chegaradan oshgan bo‘lsa, logga ogohlantirish sifatida.
Muammo: sarlavhani qachon yozamiz?
Birinchi urinish shunday bo‘lishi mumkin:
Bu ishlamaydi. next.ServeHTTP ichida handler javobni yozib bo‘lgan va sarlavhalar klientga ketib bo‘lgan. ResponseWriter va *Request darsida aytilganidek, keyin qo‘shilgan sarlavha e’tiborsiz qoladi.
Yechim — sarlavhani javob yozilishidan bir lahza oldin qo‘shish. Buning uchun ResponseWriterni o‘rab, WriteHeader chaqirilgan paytni ushlaymiz:
type timingWriter struct {
http.ResponseWriter
start time.Time
wroteHeader bool
}
func (tw *timingWriter) WriteHeader(code int) {
if !tw.wroteHeader {
tw.wroteHeader = true
dur := time.Since(tw.start)
tw.Header().Set("Server-Timing",
fmt.Sprintf("app;dur=%.2f", float64(dur.Microseconds())/1000))
}
tw.ResponseWriter.WriteHeader(code)
}
func (tw *timingWriter) Write(b []byte) (int, error) {
if !tw.wroteHeader {
tw.WriteHeader(http.StatusOK)
}
return tw.ResponseWriter.Write(b)
}
func (tw *timingWriter) Unwrap() http.ResponseWriter {
return tw.ResponseWriter
}
Server-Timing qiymati millisekundlarda yoziladi: app;dur=12.35. Bu yerdagi vaqt handler javob yozishni boshlagan paytgacha o‘lchanadi. Odatda aynan shu qism — ma’lumotni tayyorlash — eng ko‘p vaqt oladi.
Middleware
func Timing(logger *slog.Logger, slow time.Duration) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
tw := &timingWriter{ResponseWriter: w, start: start}
next.ServeHTTP(tw, r)
if dur := time.Since(start); dur > slow {
logger.Warn("sekin so‘rov",
"method", r.Method,
"path", r.URL.Path,
"duration", dur.String(),
"limit", slow.String())
}
})
}
}
Chegara (slow) parametr sifatida beriladi. Masalan, Timing(logger, 500*time.Millisecond) 500 ms dan uzoq davom etgan so‘rovlarni logga yozadi.
2. Recoverer — panicni ushlash
Handler ichida nil map’ga yozish, slice chegarasidan chiqish yoki nil ko‘rsatkichga murojaat qilish kabi xatolar panicga olib keladi.
net/http serveri har bir so‘rovdagi panicni o‘zi ushlaydi, shuning uchun butun server o‘chib qolmaydi. Lekin klient javob o‘rniga uzilgan ulanishni oladi, panic esa standart logga ro‘yxatsiz yoziladi. Biz esa klientga tushunarli 500 javobini qaytarmoqchimiz va xatoni o‘z loggerimiz orqali to‘liq yozmoqchimiz.
func Recoverer(logger *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
rec := recover()
if rec == nil {
return
}
if rec == http.ErrAbortHandler {
panic(rec) // server o‘zi ataylab ishlatadigan signal
}
logger.Error("panic",
"err", rec,
"method", r.Method,
"path", r.URL.Path,
"stack", string(debug.Stack()))
w.Header().Set("Connection", "close")
writeJSONError(w, http.StatusInternalServerError, "ichki server xatosi")
}()
next.ServeHTTP(w, r)
})
}
}
Kod qanday ishlaydi:
deferbilan e’lon qilingan funksiya handler tugagach yokipanicyuz berganda bajariladi.recover()faqatdeferichida ishlaydi.panicbo‘lgan bo‘lsa, uning qiymatini qaytaradi va dasturni normal holatga qaytaradi.debug.Stack()panicqayerda yuz berganini ko‘rsatadigan stek izini beradi. Busiz xatoni topish qiyin.http.ErrAbortHandler— handlerni ataylab to‘xtatish uchun ishlatiladigan maxsus qiymat. Uni ushlab qolmay, qaytapanicqilamiz.
Warning
panic xabarini yoki stek izini klientga yubormang. Unda fayl yo‘llari, funksiya nomlari va ichki tuzilish haqida ma’lumot bo‘ladi. Bu hujumchi uchun foydali ma’lumot. Klient faqat umumiy xabarni oladi, tafsilotlar esa logga yoziladi.
Info
Recoverer faqat so‘rovni qayta ishlayotgan goroutine’dagi panicni ushlaydi. Handler ichida go func() { ... }() bilan yangi goroutine ishga tushirilsa va u panic qilsa, butun dastur to‘xtaydi. Bunday goroutine’larda recover alohida yozilishi kerak.
3. Error handler — xatolarni markazlashtirish
CRUD loyihasida har bir handler xatoni o‘zi qayta ishlagan edi:
b, err := s.store.get(id)
if errors.Is(err, errNotFound) {
writeError(w, http.StatusNotFound, err.Error())
return
}
Handlerlar soni ko‘payganda muammolar ham ko‘payadi:
- bir xil kod ko‘p joyda takrorlanadi;
returnunutilsa, javob ikki marta yoziladi;- kimdir ichki xato matnini, masalan SQL xatosini, to‘g‘ridan-to‘g‘ri klientga qaytarib yuboradi;
- xatolarni log qilish har handlerda har xil.
Yaxshiroq yondashuv — handler xatoni qaytaradi, uni esa bitta joy qayta ishlaydi.
Xato qaytaradigan handler turi
Standart handler hech narsa qaytarmaydi. Bizning HandlerE turimiz esa error qaytaradi. Endi handler shunday yoziladi:
func (s *server) getBook(w http.ResponseWriter, r *http.Request) error {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
return BadRequest("id son bo‘lishi kerak")
}
b, err := s.store.get(id)
if err != nil {
return err
}
return writeJSON(w, http.StatusOK, b)
}
Kod ancha qisqa va tushunarli. Handler "nima noto‘g‘ri ketdi"ni aytadi, "klientga qanday javob berish"ni esa boshqa joy hal qiladi.
Xato turi: APIError
Klientga ko‘rsatiladigan xatolar uchun alohida tur yaratamiz:
type APIError struct {
Status int
Message string
}
func (e *APIError) Error() string { return e.Message }
func BadRequest(msg string) error { return &APIError{http.StatusBadRequest, msg} }
func NotFound(msg string) error { return &APIError{http.StatusNotFound, msg} }
Qoida oddiy:
- xato
*APIErrorbo‘lsa, uning status kodi va xabari klientga yuboriladi; - boshqa har qanday xato ichki xato hisoblanadi. Klient faqat
500va umumiy xabarni oladi, to‘liq xato esa logga yoziladi.
Bu qoida ichki xatolarning tasodifan tashqariga chiqishini tizim darajasida to‘xtatadi.
HandlerEni http.Handlerga aylantirish
Router http.Handler kutadi. Shuning uchun HandlerE turiga ServeHTTP metodini qo‘shamiz. Xatolarni qayta ishlash aynan shu metod ichida bo‘ladi:
func (h HandlerE) ServeHTTP(w http.ResponseWriter, r *http.Request) {
err := h(w, r)
if err == nil {
return
}
var apiErr *APIError
if errors.As(err, &apiErr) {
writeJSONError(w, apiErr.Status, apiErr.Message)
return
}
slog.Error("ichki xato", "err", err, "method", r.Method, "path", r.URL.Path)
writeJSONError(w, http.StatusInternalServerError, "ichki server xatosi")
}
errors.As xatolar zanjiri ichidan *APIErrorni qidiradi. Xato fmt.Errorf("...: %w", apiErr) bilan o‘ralgan bo‘lsa ham topiladi.
Bu usul http.HandlerFunc bilan bir xil g‘oyaga asoslanadi: funksiya turiga ServeHTTP metodi qo‘shiladi. Farqi shundaki, bizning metodimiz xatoni ham qayta ishlaydi.
Hammasi birga
| main.go | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 | |
Middleware’lar tartibiga e’tibor bering. Recoverer eng oxirida o‘ralgan, demak u eng tashqi qatlam. U Timing va barcha handlerlardagi panicni ushlaydi.
Sinab ko‘rish
curl -i http://localhost:8080/books/1
curl http://localhost:8080/books/abc
curl http://localhost:8080/books/9
curl http://localhost:8080/slow
curl http://localhost:8080/db
curl http://localhost:8080/crash
Klient ko‘radigan javoblar:
HTTP/1.1 200 OK
Content-Type: application/json
Server-Timing: app;dur=0.03
...
{"id":1,"title":"O‘tkan kunlar"}
{"error":"id son bo‘lishi kerak"}
{"error":"kitob topilmadi"}
{"status":"tayyor"}
{"error":"ichki server xatosi"}
{"error":"ichki server xatosi"}
Server loglari (qisqartirilgan):
{"level":"WARN","msg":"sekin so‘rov","method":"GET","path":"/slow","duration":"300.61ms","limit":"200ms"}
{"level":"ERROR","msg":"ichki xato","err":"kitoblarni o‘qish: pq: connection refused","method":"GET","path":"/db"}
{"level":"ERROR","msg":"panic","err":"assignment to entry in nil map","method":"GET","path":"/crash","stack":"goroutine 7 [running]:\n..."}
Natijalar:
/dbso‘rovida klient faqat umumiy xabarni oldi. SQL xatosining to‘liq matni esa faqat logda./crashso‘rovidagipanicushlandi va server ishlashda davom etdi. Keyingi so‘rovlar odatdagidek bajariladi./slowso‘rovi 200 ms chegarasidan oshgani uchun logga ogohlantirish tushdi.
Xulosa
- Timing middleware
ResponseWriterni o‘rab,Server-Timingsarlavhasini javob yuborilishidan oldin qo‘shadi. - Recoverer
panicni ushlab, klientga500qaytaradi va stek izini logga yozadi. U zanjirning eng tashqi qatlami bo‘lishi kerak. HandlerEturi handlerlarga xatoni qaytarish imkonini beradi. Xatolar bitta joyda qayta ishlanadi.*APIErrorklientga ko‘rsatiladi, qolgan barcha xatolar esa500sifatida yashiriladi va logga yoziladi.
Keyingi darsda loyiha kodini qatlamlarga ajratib, clean architecture tamoyillari bilan tanishamiz.