Tarkibga o'tish

401, 403 va 500 statuslar

Himoyalangan API’da so‘rov turli sabablarga ko‘ra rad etilishi mumkin: token yo‘q, token eskirgan, ruxsat yetarli emas, resurs boshqa odamniki yoki serverda xato yuz bergan. Har bir holat uchun to‘g‘ri status kod tanlash muhim.

To‘g‘ri status kod klientga keyin nima qilish kerakligini aytadi:

  • 401 → "qayta login qiling yoki tokenni yangilang";
  • 403 → "qayta urinishning foydasi yo‘q, sizda ruxsat yo‘q";
  • 500 → "sizda xato yo‘q, muammo serverda, keyinroq urinib ko‘ring".

Noto‘g‘ri kod esa klientni adashtiradi. Masalan, ruxsat yo‘qligi uchun 401 qaytarilsa, mobil ilova foydalanuvchini qayta-qayta login sahifasiga yuboradi. Foydalanuvchi to‘g‘ri parol kiritadi, lekin baribir ichkariga kira olmaydi.

401 Unauthorized — "sizni tanimadim"

Nomiga qaramay, 401 avtorizatsiya haqida emas, autentifikatsiya haqida. Server so‘rov yuboruvchini aniqlay olmadi.

Qachon qaytariladi:

  • Authorization sarlavhasi yo‘q;
  • token formati noto‘g‘ri yoki imzosi buzilgan;
  • token muddati tugagan;
  • login paytida email yoki parol noto‘g‘ri;
  • token egasi bazadan o‘chirilgan yoki bloklangan.

HTTP standarti (RFC 9110) bo‘yicha 401 javobida WWW-Authenticate sarlavhasi bo‘lishi shart. U qanday autentifikatsiya kutilayotganini bildiradi:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="token muddati tugagan"
Content-Type: application/json

{"error":{"code":"token_expired","message":"token muddati tugagan"}}

Klient uchun muhim jihat: muddati tugagan token qayta tiklanadigan holat. Klient refresh token bilan yangi access token oladi va so‘rovni takrorlaydi. Shu sababli token_expired holatini boshqa 401 sabablaridan alohida kod bilan ajratish foydali.

403 Forbidden — "tanidim, lekin ruxsat yo‘q"

Server foydalanuvchini aniq biladi, lekin unga bu amalni bajarishga ruxsat bermaydi.

Qachon qaytariladi:

  • rolida kerakli ruxsat yo‘q: reader kitob o‘chirmoqchi;
  • resurs boshqa odamniki, lekin foydalanuvchi uni ko‘ra oladi: nashr qilingan begona kitobni tahrirlash;
  • biznes qoidasi taqiqlaydi: nashr qilingan kitobni o‘chirib bo‘lmaydi;
  • akkaunt tasdiqlanmagan: email hali tasdiqlanmagan.

403 olgan klient qayta login qilmasligi kerak. Boshqa token bilan ham natija o‘zgarmaydi. Foydalanuvchiga tushunarli xabar ko‘rsatish kifoya: "Bu amal uchun ruxsatingiz yo‘q".

404 Not Found — resursni yashirish

Resource-based permission darsida aytilganidek, ba’zan 403 o‘rniga 404 qaytarish xavfsizroq:

GET /orders/1002   (Boburning buyurtmasi, so‘rovni Aziza yubordi)

403 → "Bu buyurtma bor, lekin sizniki emas"   ← mavjudlik oshkor bo‘ldi
404 → "Bunday buyurtma yo‘q"                   ← hech narsa oshkor bo‘lmadi

403 javobidan hujumchi qaysi ID’lar mavjudligini bilib oladi. Bu ma’lumot keyingi hujumlar uchun foydali bo‘lishi mumkin: buyurtmalar soni, yangi foydalanuvchilar, maxfiy loyihalar nomlari.

Qoida: foydalanuvchi resursni ko‘rish huquqiga ega bo‘lmasa, 404 qaytariladi. Ko‘ra oladigan, lekin o‘zgartira olmaydigan bo‘lsa, 403 qaytariladi.

500 Internal Server Error — "muammo bizda"

500 klient xatosi emas. U server kutilmagan holatga duch kelganini bildiradi: baza ishlamayapti, kodda panic yuz berdi yoki tashqi servis javob bermadi.

Auth kodidagi 500 xavfi

Avtorizatsiya kodidagi xato ikki xil yakun topishi mumkin: ochiq qolish (fail open) yoki yopiq qolish (fail closed).

// ❌ Fail open: baza xatosida ruxsat berib yuboriladi
func canEdit(ctx context.Context, userID, bookID int64) bool {
    owner, err := repo.OwnerOf(ctx, bookID)
    if err != nil {
        log.Println(err)
        return true // "baza ishlamayapti, mayli, o‘tkazib yuboramiz"
    }
    return owner == userID
}

// ✅ Fail closed: xato — ruxsat yo‘q, lekin xato yashirilmaydi
func canEdit(ctx context.Context, userID, bookID int64) (bool, error) {
    owner, err := repo.OwnerOf(ctx, bookID)
    if err != nil {
        return false, fmt.Errorf("kitob egasini aniqlash: %w", err)
    }
    return owner == userID, nil
}

Birinchi variantda bazadagi vaqtinchalik uzilish hamma uchun eshikni ochib qo‘yadi. Ikkinchisida esa xato yuqoriga qaytariladi, handler 500 beradi va hech kim ruxsatsiz o‘tib ketmaydi.

Xatoni 403 sifatida qaytarish ham noto‘g‘ri. Foydalanuvchida ruxsat bor, lekin server buni tekshira olmadi. 403 uni chalg‘itadi, loglarda esa haqiqiy muammo "ruxsat yo‘q" holatlari orasida yo‘qolib ketadi.

Ichki tafsilotlarni yashirish

500 javobida xato matni klientga yuborilmaydi:

❌ {"error": "pq: relation \"user_permissions\" does not exist"}
✅ {"error": {"code": "internal", "message": "Ichki server xatosi", "request_id": "5c1e0a9b"}}

Birinchi javob jadval nomini va database turini oshkor qiladi. To‘liq xato faqat logga yoziladi. Klient esa loglardan kerakli yozuvni topish uchun request_id oladi. Bu yondashuv Xatoliklarni standart formatda qaytarish darsida amalga oshirilgan.

Boshqa tegishli status kodlar

Kod Qachon
400 Bad Request So‘rov formati noto‘g‘ri, masalan JSON buzilgan
409 Conflict Holat ziddiyati, masalan email band
422 Unprocessable Entity Validatsiya xatosi
429 Too Many Requests So‘rovlar chegarasidan oshildi. Login’ga urinishlar ham shunga kiradi
503 Service Unavailable Server vaqtincha ishlamayapti yoki haddan tashqari band

Qaror daraxti

So‘rov keldi
│
├─ Serverda kutilmagan xato bo‘ldimi? ─────────────────── ha → 500
│
├─ Foydalanuvchi aniqlandimi (token to‘g‘ri)? ─────────── yo‘q → 401
│
├─ Foydalanuvchi bu resursni ko‘ra oladimi? ───────────── yo‘q → 404
│
├─ Foydalanuvchi bu amalni bajara oladimi? ────────────── yo‘q → 403
│
└─ Hammasi joyida ─────────────────────────────────────────────→ 2xx

Go’da: xatolarni status kodga moslash

Service qatlami HTTP’ni bilmaydi. U faqat domen xatolarini qaytaradi. Ularni status kodga aylantirish HTTP qatlamida, bitta joyda bajariladi:

main.go
package main

import (
    "context"
    "errors"
    "fmt"
    "log/slog"
    "net/http"
    "net/http/httptest"
    "os"
)

var (
    ErrUnauthenticated = errors.New("unauthenticated")
    ErrTokenExpired    = errors.New("token expired")
    ErrForbidden       = errors.New("forbidden")
    ErrNotFound        = errors.New("not found")
)

func statusFor(err error) (status int, code, msg string) {
    switch {
    case errors.Is(err, ErrTokenExpired):
        return http.StatusUnauthorized, "token_expired", "token muddati tugagan"
    case errors.Is(err, ErrUnauthenticated):
        return http.StatusUnauthorized, "unauthorized", "tizimga kiring"
    case errors.Is(err, ErrForbidden):
        return http.StatusForbidden, "forbidden", "bu amal uchun ruxsat yo‘q"
    case errors.Is(err, ErrNotFound):
        return http.StatusNotFound, "not_found", "resurs topilmadi"
    case errors.Is(err, context.Canceled):
        return 499, "client_closed", "" // klient ketdi, javob o‘qilmaydi
    default:
        return http.StatusInternalServerError, "internal", "ichki server xatosi"
    }
}

func WriteError(w http.ResponseWriter, r *http.Request, err error) {
    status, code, msg := statusFor(err)

    switch {
    case status >= 500:
        slog.Error("server xatosi", "err", err, "path", r.URL.Path)
    case status == http.StatusForbidden:
        // Rad etilgan urinishlar xavfsizlik tahlili uchun foydali
        slog.Warn("ruxsat rad etildi", "path", r.URL.Path, "method", r.Method)
    }

    if status == http.StatusUnauthorized {
        w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
    }
    if status == 499 {
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    fmt.Fprintf(w, `{"error":{"code":%q,"message":%q}}`+"\n", code, msg)
}

func main() {
    slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
        ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
            if a.Key == slog.TimeKey {
                return slog.Attr{} // misol natijasi qisqa bo‘lishi uchun vaqt yozilmaydi
            }
            return a
        },
    })))

    errs := []error{
        ErrUnauthenticated,
        fmt.Errorf("token tekshiruvi: %w", ErrTokenExpired),
        ErrForbidden,
        ErrNotFound,
        errors.New("pq: connection refused"),
    }

    for _, e := range errs {
        rec := httptest.NewRecorder()
        WriteError(rec, httptest.NewRequest("DELETE", "/books/42", nil), e)
        fmt.Printf("%-40q → %d %s", e.Error(), rec.Code, rec.Body.String())
    }
}
"unauthenticated"                        → 401 {"error":{"code":"unauthorized","message":"tizimga kiring"}}
"token tekshiruvi: token expired"        → 401 {"error":{"code":"token_expired","message":"token muddati tugagan"}}
level=WARN msg="ruxsat rad etildi" path=/books/42 method=DELETE
"forbidden"                              → 403 {"error":{"code":"forbidden","message":"bu amal uchun ruxsat yo‘q"}}
"not found"                              → 404 {"error":{"code":"not_found","message":"resurs topilmadi"}}
level=ERROR msg="server xatosi" err="pq: connection refused" path=/books/42
"pq: connection refused"                 → 500 {"error":{"code":"internal","message":"ichki server xatosi"}}

E’tibor bering:

  • O‘ralgan xato (token tekshiruvi: ...) ham errors.Is orqali to‘g‘ri tanildi.
  • 403 holatlari WARN darajasida logga yozildi. Bitta foydalanuvchidan qisqa vaqt ichida kelgan ko‘plab 403 javoblari kimdir tizimni "paypaslab" ko‘rayotganidan dalolat berishi mumkin.
  • Noma’lum xato klientga faqat umumiy xabar bilan qaytdi. To‘liq matn esa logda qoldi.
  • 499 rasmiy HTTP status kodi emas. Uni nginx klient ulanishni uzgan holatlar uchun ishlatadi. Bu holatda javob yozilmaydi, chunki uni o‘qiydigan hech kim yo‘q.

Xulosa

Status Ma’nosi Klient nima qiladi
401 Sizni tanimadim Login qiladi yoki tokenni yangilaydi
403 Tanidim, ruxsat yo‘q Xabar ko‘rsatadi, qayta urinmaydi
404 Topilmadi yoki ko‘rishga ruxsat yo‘q Xabar ko‘rsatadi
500 Server xatosi Keyinroq urinib ko‘radi
  • 401 javobida WWW-Authenticate sarlavhasi bo‘ladi.
  • Resurs mavjudligini yashirish kerak bo‘lsa, 403 o‘rniga 404 qaytariladi.
  • Auth kodidagi xato ruxsat berib yubormasligi kerak. Xato — 500, ruxsat esa rad etiladi.
  • Ichki xato tafsilotlari faqat logga yoziladi.

Shu bilan avtorizatsiya bo‘limi tugadi. Keyingi bo‘limda standart router o‘rniga gorilla/mux kutubxonasi bilan ishlashni ko‘ramiz.