Tarkibga o'tish

Request timing va error handler middleware

Bu darsda API’ni barqaror qiladigan uchta middleware yozamiz:

  1. Timing — har bir so‘rov qancha vaqt olganini o‘lchaydi va sekin so‘rovlarni aniqlaydi.
  2. Recoverer — handler ichidagi panicni ushlaydi va serverning "yiqilishi"ga yo‘l qo‘ymaydi.
  3. 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-Timing sarlavhasiga. 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:

start := time.Now()
next.ServeHTTP(w, r)
w.Header().Set("Server-Timing", ...) // kech!

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:

  • defer bilan e’lon qilingan funksiya handler tugagach yoki panic yuz berganda bajariladi.
  • recover() faqat defer ichida ishlaydi. panic bo‘lgan bo‘lsa, uning qiymatini qaytaradi va dasturni normal holatga qaytaradi.
  • debug.Stack() panic qayerda yuz berganini ko‘rsatadigan stek izini beradi. Busiz xatoni topish qiyin.
  • http.ErrAbortHandler — handlerni ataylab to‘xtatish uchun ishlatiladigan maxsus qiymat. Uni ushlab qolmay, qayta panic qilamiz.

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;
  • return unutilsa, 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

type HandlerE func(w http.ResponseWriter, r *http.Request) error

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 *APIError bo‘lsa, uning status kodi va xabari klientga yuboriladi;
  • boshqa har qanday xato ichki xato hisoblanadi. Klient faqat 500 va 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
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "runtime/debug"
    "strconv"
    "time"
)

// ---------- Xatolar ----------

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} }

func writeJSON(w http.ResponseWriter, status int, v any) error {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    return json.NewEncoder(w).Encode(v)
}

func writeJSONError(w http.ResponseWriter, status int, msg string) {
    _ = writeJSON(w, status, map[string]string{"error": msg})
}

// ---------- Xato qaytaradigan handler ----------

type HandlerE func(w http.ResponseWriter, r *http.Request) error

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")
}

// ---------- Timing ----------

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
}

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())
            }
        })
    }
}

// ---------- Recoverer ----------

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)
                }

                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)
        })
    }
}

// ---------- Handlerlar ----------

var books = map[int]string{1: "O‘tkan kunlar", 2: "Kecha va kunduz"}

func 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")
    }

    title, ok := books[id]
    if !ok {
        return NotFound("kitob topilmadi")
    }

    return writeJSON(w, http.StatusOK, map[string]any{"id": id, "title": title})
}

func slow(w http.ResponseWriter, r *http.Request) error {
    time.Sleep(300 * time.Millisecond)
    return writeJSON(w, http.StatusOK, map[string]string{"status": "tayyor"})
}

func dbError(w http.ResponseWriter, r *http.Request) error {
    return fmt.Errorf("kitoblarni o‘qish: %w", errors.New("pq: connection refused"))
}

func crash(w http.ResponseWriter, r *http.Request) error {
    var m map[string]int
    m["x"] = 1 // nil map'ga yozish — panic
    return nil
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    slog.SetDefault(logger)

    mux := http.NewServeMux()
    mux.Handle("GET /books/{id}", HandlerE(getBook))
    mux.Handle("GET /slow", HandlerE(slow))
    mux.Handle("GET /db", HandlerE(dbError))
    mux.Handle("GET /crash", HandlerE(crash))

    var handler http.Handler = mux
    handler = Timing(logger, 200*time.Millisecond)(handler)
    handler = Recoverer(logger)(handler)

    logger.Info("server ishga tushdi", "addr", ":8080")
    if err := http.ListenAndServe(":8080", handler); err != nil {
        logger.Error("server to‘xtadi", "err", err)
        os.Exit(1)
    }
}

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:

  • /db so‘rovida klient faqat umumiy xabarni oldi. SQL xatosining to‘liq matni esa faqat logda.
  • /crash so‘rovidagi panic ushlandi va server ishlashda davom etdi. Keyingi so‘rovlar odatdagidek bajariladi.
  • /slow so‘rovi 200 ms chegarasidan oshgani uchun logga ogohlantirish tushdi.

Xulosa

  • Timing middleware ResponseWriterni o‘rab, Server-Timing sarlavhasini javob yuborilishidan oldin qo‘shadi.
  • Recoverer panicni ushlab, klientga 500 qaytaradi va stek izini logga yozadi. U zanjirning eng tashqi qatlami bo‘lishi kerak.
  • HandlerE turi handlerlarga xatoni qaytarish imkonini beradi. Xatolar bitta joyda qayta ishlanadi.
  • *APIError klientga ko‘rsatiladi, qolgan barcha xatolar esa 500 sifatida yashiriladi va logga yoziladi.

Keyingi darsda loyiha kodini qatlamlarga ajratib, clean architecture tamoyillari bilan tanishamiz.