Tarkibga o'tish

Xatoliklarni standart formatda qaytarish

Muvaffaqiyatli javoblar kabi xato javoblari ham izchil bo‘lishi kerak. Klient dasturi har qanday xatoni bitta usul bilan qayta ishlay olishi lozim. Tasavvur qiling, API’da xatolar quyidagicha qaytadi:

POST /books      → 400  kitob nomi majburiy               (oddiy matn)
GET  /books/99   → 404  {"message": "not found"}
POST /login      → 401  {"error": true, "msg": "Xato parol"}
POST /orders     → 422  {"errors": [{"field": "qty", ...}]}

Har bir endpoint o‘z uslubida javob beryapti. Mobil ilova dasturchisi har bir holat uchun alohida kod yozishga majbur bo‘ladi. Bu darsda barcha xatolar uchun yagona format tanlaymiz va uni Go’da amalga oshiramiz.

Yaxshi xato javobida nima bo‘lishi kerak?

Maydon Kim uchun Nima uchun
HTTP status kod Dastur Xato turini umumiy ko‘rinishda bildiradi
Mashina o‘qiydigan kod Dastur Aniq holatni ajratadi: email_taken, out_of_stock
Xabar Inson Nima bo‘lganini tushuntiradi
Maydon xatolari Dastur va inson Formadagi qaysi maydon noto‘g‘ri ekanini ko‘rsatadi
Request ID Qo‘llab-quvvatlash xizmati Xatoni loglardan topishga yordam beradi

Nima uchun xabar yetarli emas? Klient dasturi xabar matniga qarab qaror qilmasligi kerak. Xabar o‘zgarishi, tarjima qilinishi yoki aniqlashtirilishi mumkin. Mashina o‘qiydigan kod esa barqaror shartnoma hisoblanadi:

// ❌ Mo‘rt: xabar o‘zgarsa, kod ishlamay qoladi
if (err.message === "Bu email band") { ... }

// ✅ Barqaror
if (err.code === "email_taken") { ... }

Format

Bu kursda quyidagi formatdan foydalanamiz:

{
  "error": {
    "code": "validation_failed",
    "message": "Ma’lumotlar noto‘g‘ri",
    "fields": {
      "email": "email formati noto‘g‘ri",
      "age": "14 dan kichik bo‘lmasligi kerak"
    },
    "request_id": "5c1e0a9b3d7f2e84"
  }
}
  • Xato har doim error kaliti ichida keladi. Muvaffaqiyatli javob esa data ichida keladi. Klient qaysi holat ekanini kalitning o‘zidan ham biladi.
  • fields faqat validatsiya xatolarida bo‘ladi.
  • request_id logging middleware yaratgan ID bilan bir xil.

Xato kodlari katalogi

Kodlarni tasodifiy satrlar sifatida emas, bitta joyda konstantalar ko‘rinishida e’lon qilamiz:

Kod Status Qachon
bad_request 400 So‘rov formati noto‘g‘ri, JSON buzilgan
validation_failed 422 Maydonlar qoidalarga mos emas
unauthorized 401 Autentifikatsiya yo‘q yoki token yaroqsiz
forbidden 403 Ruxsat yo‘q
not_found 404 Resurs topilmadi
conflict 409 Holat ziddiyati: dublikat, eskirgan versiya
rate_limited 429 So‘rovlar juda ko‘p
internal 500 Kutilmagan server xatosi

Kerak bo‘lsa, aniqroq kodlar qo‘shiladi: email_taken, book_out_of_stock. Ularni API hujjatida albatta yozib qo‘ying.

Go’da amalga oshirish

Xato turi

type Error struct {
    Status  int               `json:"-"`
    Code    string            `json:"code"`
    Message string            `json:"message"`
    Fields  map[string]string `json:"fields,omitempty"`
    Err     error             `json:"-"` // ichki sabab, klientga ko‘rsatilmaydi
}

func (e *Error) Error() string {
    if e.Err != nil {
        return e.Code + ": " + e.Err.Error()
    }
    return e.Code + ": " + e.Message
}

func (e *Error) Unwrap() error { return e.Err }
  • Status va Err json:"-" bilan belgilangan. Status kod HTTP sarlavhada yuboriladi, ichki sabab esa faqat logga yoziladi.
  • Unwrap metodi errors.Is va errors.As funksiyalariga ichki xatoni ko‘rish imkonini beradi.

Konstruktorlar

func NotFound(what string) *Error {
    return &Error{Status: http.StatusNotFound, Code: "not_found",
        Message: what + " topilmadi"}
}

func Validation(fields map[string]string) *Error {
    return &Error{Status: http.StatusUnprocessableEntity, Code: "validation_failed",
        Message: "Ma’lumotlar noto‘g‘ri", Fields: fields}
}

func Conflict(code, msg string) *Error {
    return &Error{Status: http.StatusConflict, Code: code, Message: msg}
}

func Internal(err error) *Error {
    return &Error{Status: http.StatusInternalServerError, Code: "internal",
        Message: "Ichki server xatosi", Err: err}
}

Handler endi return NotFound("kitob") deb yozadi va status kod, kod nomi hamda xabarni har safar qaytadan o‘ylamaydi.

Xatoni yozadigan yagona funksiya

func WriteError(w http.ResponseWriter, r *http.Request, err error) {
    var e *Error
    if !errors.As(err, &e) {
        e = Internal(err) // noma’lum xato — har doim 500
    }

    if e.Status >= 500 {
        slog.Error("server xatosi", "err", e, "path", r.URL.Path,
            "request_id", RequestID(r.Context()))
    }

    body := struct {
        Error struct {
            *Error
            RequestID string `json:"request_id,omitempty"`
        } `json:"error"`
    }{}
    body.Error.Error = e
    body.Error.RequestID = RequestID(r.Context())

    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(e.Status)
    json.NewEncoder(w).Encode(body)
}

WriteError ikki narsani kafolatlaydi:

  1. Har qanday xato bir xil formatda qaytadi.
  2. *Error bo‘lmagan xato, masalan database xatosi, hech qachon klientga ko‘rsatilmaydi. U internal kodi bilan yashiriladi va logga yoziladi.

To‘liq misol

main.go
package main

import (
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "log/slog"
    "net/http"
    "os"
    "strconv"
    "strings"
    "sync/atomic"
)

// ---------- apierr ----------

type Error struct {
    Status  int               `json:"-"`
    Code    string            `json:"code"`
    Message string            `json:"message"`
    Fields  map[string]string `json:"fields,omitempty"`
    Err     error             `json:"-"`
}

func (e *Error) Error() string {
    if e.Err != nil {
        return e.Code + ": " + e.Err.Error()
    }
    return e.Code + ": " + e.Message
}

func (e *Error) Unwrap() error { return e.Err }

func BadRequest(msg string) *Error {
    return &Error{Status: http.StatusBadRequest, Code: "bad_request", Message: msg}
}

func NotFound(what string) *Error {
    return &Error{Status: http.StatusNotFound, Code: "not_found", Message: what + " topilmadi"}
}

func Validation(fields map[string]string) *Error {
    return &Error{Status: http.StatusUnprocessableEntity, Code: "validation_failed",
        Message: "Ma’lumotlar noto‘g‘ri", Fields: fields}
}

func Conflict(code, msg string) *Error {
    return &Error{Status: http.StatusConflict, Code: code, Message: msg}
}

func Internal(err error) *Error {
    return &Error{Status: http.StatusInternalServerError, Code: "internal",
        Message: "Ichki server xatosi", Err: err}
}

// ---------- Request ID (soddalashtirilgan) ----------

type ctxKey struct{}

func RequestID(ctx context.Context) string {
    id, _ := ctx.Value(ctxKey{}).(string)
    return id
}

func withRequestID(next http.Handler) http.Handler {
    var n atomic.Int64
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := fmt.Sprintf("req-%03d", n.Add(1))
        ctx := context.WithValue(r.Context(), ctxKey{}, id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// ---------- Javob yozish ----------

func WriteError(w http.ResponseWriter, r *http.Request, err error) {
    var e *Error
    if !errors.As(err, &e) {
        e = Internal(err)
    }

    if e.Status >= 500 {
        slog.Error("server xatosi", "err", e, "path", r.URL.Path,
            "request_id", RequestID(r.Context()))
    }

    body := struct {
        Error struct {
            *Error
            RequestID string `json:"request_id,omitempty"`
        } `json:"error"`
    }{}
    body.Error.Error = e
    body.Error.RequestID = RequestID(r.Context())

    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(e.Status)
    json.NewEncoder(w).Encode(body)
}

// HandlerE — xato qaytaradigan handler.
type HandlerE func(w http.ResponseWriter, r *http.Request) error

func (h HandlerE) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := h(w, r); err != nil {
        WriteError(w, r, err)
    }
}

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

var titles = map[int]string{1: "O‘tkan kunlar"}

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 := titles[id]
    if !ok {
        return NotFound("kitob")
    }
    w.Header().Set("Content-Type", "application/json")
    return json.NewEncoder(w).Encode(map[string]any{"data": map[string]any{"id": id, "title": title}})
}

func createBook(w http.ResponseWriter, r *http.Request) error {
    var in struct {
        Title string `json:"title"`
        Year  int    `json:"year"`
    }
    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
        return BadRequest("JSON noto‘g‘ri")
    }

    fields := map[string]string{}
    if strings.TrimSpace(in.Title) == "" {
        fields["title"] = "majburiy maydon"
    }
    if in.Year < 1 {
        fields["year"] = "musbat son bo‘lishi kerak"
    }
    if len(fields) > 0 {
        return Validation(fields)
    }

    for _, t := range titles {
        if strings.EqualFold(t, in.Title) {
            return Conflict("book_exists", "Bunday nomli kitob allaqachon mavjud")
        }
    }

    return fmt.Errorf("kitobni saqlash: %w", errors.New("pq: disk is full"))
}

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

    mux := http.NewServeMux()
    mux.Handle("GET /books/{id}", HandlerE(getBook))
    mux.Handle("POST /books", HandlerE(createBook))

    slog.Info("server ishga tushdi", "addr", ":8080")
    if err := http.ListenAndServe(":8080", withRequestID(mux)); err != nil {
        slog.Error("server to‘xtadi", "err", err)
    }
}

Note

Natijani o‘qish oson bo‘lishi uchun misoldagi withRequestID ketma-ket raqamlar (req-001, req-002) yaratadi. Hisoblagich atomic.Int64 turida, shuning uchun bir vaqtda kelgan so‘rovlarda ham xavfsiz. Real loyihada esa Logging middleware darsidagi tasodifiy ID ishlatiladi.

Sinab ko‘ramiz:

curl http://localhost:8080/books/abc
curl http://localhost:8080/books/7
curl -X POST http://localhost:8080/books -d '{"title":" ","year":0}'
curl -X POST http://localhost:8080/books -d '{"title":"o‘tkan kunlar","year":1926}'
curl -X POST http://localhost:8080/books -d '{"title":"Sarob","year":1937}'
{"error":{"code":"bad_request","message":"id son bo‘lishi kerak","request_id":"req-001"}}
{"error":{"code":"not_found","message":"kitob topilmadi","request_id":"req-002"}}
{"error":{"code":"validation_failed","message":"Ma’lumotlar noto‘g‘ri","fields":{"title":"majburiy maydon","year":"musbat son bo‘lishi kerak"},"request_id":"req-003"}}
{"error":{"code":"book_exists","message":"Bunday nomli kitob allaqachon mavjud","request_id":"req-004"}}
{"error":{"code":"internal","message":"Ichki server xatosi","request_id":"req-005"}}

Oxirgi so‘rovda database xatosi yuz berdi. Klient faqat umumiy xabar va request ID’ni oldi. Server logida esa to‘liq sabab bor:

{"level":"ERROR","msg":"server xatosi","err":"internal: kitobni saqlash: pq: disk is full","path":"/books","request_id":"req-005"}

Foydalanuvchi qo‘llab-quvvatlash xizmatiga req-005 raqamini aytsa, muammo loglardan darhol topiladi.

Standart: RFC 9457 Problem Details

HTTP API xatolari uchun rasmiy standart ham bor: RFC 9457 — "Problem Details for HTTP APIs". U avvalgi RFC 7807 standartining o‘rnini egallagan. Unda javob application/problem+json turida yuboriladi:

{
  "type": "https://api.example.uz/errors/validation-failed",
  "title": "Ma’lumotlar noto‘g‘ri",
  "status": 422,
  "detail": "Ikki maydon qoidalarga mos kelmadi",
  "instance": "/books",
  "errors": {"title": "majburiy maydon"}
}
Maydon Ma’nosi
type Xato turini aniqlovchi URI. Uning manzilida xato haqida hujjat bo‘lishi mumkin
title Xato turining qisqa nomi
status HTTP status kod
detail Aynan shu holat bo‘yicha tushuntirish
instance Xato yuz bergan resurs

Standartda qo‘shimcha maydonlar qo‘shishga ruxsat berilgan, masalan yuqoridagi errors.

Qaysi formatni tanlash kerak? Agar API ko‘p tashqi jamoalar uchun ochiq bo‘lsa, standart format ular uchun qulayroq. Ichki API uchun bu darsdagi soddaroq {"error": {...}} formati ham yetarli. Eng muhimi — tanlangan formatni barcha endpointlarda izchil qo‘llash.

Yuqoridagi WriteError funksiyasini Problem Details formatiga o‘tkazish uchun faqat JSON tuzilishini va Content-Type sarlavhasini o‘zgartirish kifoya. Handlerlar o‘zgarmaydi, chunki ular faqat *Error qaytaradi.

Xulosa

  • Xato javobida status kod, mashina o‘qiydigan code, inson uchun message va kerak bo‘lsa fields bo‘lsin.
  • Klient kodi xabar matniga emas, code qiymatiga tayanishi kerak.
  • Barcha xatolar bitta WriteError funksiyasi orqali yozilsin.
  • Noma’lum xatolar doim 500 va umumiy xabar bilan yashiriladi. Tafsilotlar logga yoziladi.
  • Request ID klient ko‘rgan xatoni server logi bilan bog‘laydi.
  • Ommaviy API uchun RFC 9457 Problem Details standartini ko‘rib chiqing.

Shu bilan JSON bo‘limi tugadi. Keyingi bo‘limda ma’lumotlarni PostgreSQL’da saqlashni o‘rganamiz.