Tarkibga o'tish

Request body parsing

POST va PUT so‘rovlarida klient ma’lumotni so‘rov tanasida (request body) JSON ko‘rinishida yuboradi. Uni Go struct’iga o‘qish bitta qatorda bajariladi:

json.NewDecoder(r.Body).Decode(&input)

Oddiy ko‘rinadi. Lekin server internetga ochiq bo‘lsa, unga faqat yaxshi niyatli klientlar murojaat qilmaydi. Kimdir 5 GB hajmli tana yuboradi, kimdir JSON o‘rniga matn yuboradi, kimdir maydon nomini xato yozadi. Bu darsda shu holatlarning barchasini to‘g‘ri qayta ishlaydigan decodeJSON funksiyasini bosqichma-bosqich yozamiz.

Oddiy usulning muammolari

func createBook(w http.ResponseWriter, r *http.Request) {
    var in BookInput
    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    // ...
}

Bu kodda beshta muammo bor:

  1. Hajm cheklanmagan. Katta tana server xotirasini to‘ldirishi mumkin.
  2. Notanish maydonlar jimgina tashlab yuboriladi. Klient "titel" deb xato yozsa, Title bo‘sh qoladi va xato sababi tushunarsiz bo‘ladi.
  3. Ortiqcha ma’lumot tekshirilmaydi. {"title":"A"}{"title":"B"} yoki {"title":"A"} qandaydir matn ham xatosiz qabul qilinadi, chunki Decode faqat birinchi JSON qiymatni o‘qiydi.
  4. Xato xabarlari texnik. Klient json: cannot unmarshal string into Go struct field BookInput.year of type int kabi xabarni oladi. Unda Go’dagi tur nomlari ham ko‘rinib qoladi.
  5. Content-Type tekshirilmaydi.

Endi ularni birma-bir hal qilamiz.

1. Hajmni cheklash

r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MB

http.MaxBytesReader tanani o‘rab oladi. Belgilangan chegaradan ko‘p o‘qishga urinilsa, *http.MaxBytesError xatosini qaytaradi. Bundan tashqari, serverga ulanishni yopish kerakligi haqida signal beradi.

Chegara endpointga qarab tanlanadi. Oddiy JSON so‘rovlar uchun 1 MB yetarli bo‘ladi. Fayl yuklash endpointlari uchun esa kattaroq chegara belgilanadi.

2. Notanish maydonlarni rad etish

dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()

Endi {"titel": "Sarob"} so‘rovi json: unknown field "titel" xatosini beradi. Klient dasturchisi xatoni darhol ko‘radi.

Info

DisallowUnknownFields API’ni qat’iyroq qiladi. Lekin bir kamchiligi bor: kelajakda klient yangi maydon yuborsa-yu, server hali yangilanmagan bo‘lsa, so‘rov rad etiladi. Ko‘pchilik ichki va ommaviy API’lar uchun qat’iylik foydaliroq. Shunday bo‘lsa-da, bu qarorni ongli ravishda qabul qiling.

3. Ortiqcha ma’lumotni tekshirish

Decode birinchi JSON qiymatni o‘qigach to‘xtaydi. Tanada boshqa ma’lumot qolmaganini tekshirish uchun yana bir marta o‘qishga urinamiz:

if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
    return errors.New("tanada faqat bitta JSON obyekt bo‘lishi kerak")
}

Ikkinchi Decode io.EOF qaytarsa, tana tugagan. Boshqa har qanday natija tanada ortiqcha ma’lumot borligini bildiradi.

4. Xatolarni tushunarli qilish

encoding/json qaytaradigan xatolar bir necha turga bo‘linadi. Ularni errors.As bilan ajratib, har biriga mos xabar tayyorlaymiz:

Xato Qachon yuz beradi Klientga xabar
*json.SyntaxError JSON sintaksisi buzilgan "JSON 15-baytda noto‘g‘ri"
io.ErrUnexpectedEOF JSON oxiriga yetmay uzilgan "JSON to‘liq emas"
*json.UnmarshalTypeError Maydon turi mos emas "year maydoni son bo‘lishi kerak"
io.EOF Tana bo‘sh "so‘rov tanasi bo‘sh"
*http.MaxBytesError Tana juda katta "tana 1048576 baytdan oshmasligi kerak"
json: unknown field Notanish maydon "notanish maydon: titel"

UnmarshalTypeErrorning Field maydonida xato qaysi JSON maydonida yuz bergani yoziladi. Ichki maydonlar uchun u author.name ko‘rinishida bo‘ladi.

Notanish maydon xatosi alohida tur sifatida e’lon qilinmagan. Uni faqat xato matni orqali aniqlash mumkin. Bu encoding/json paketining ma’lum cheklovi.

5. Content-Typeni tekshirish

ct := r.Header.Get("Content-Type")
mediaType, _, _ := mime.ParseMediaType(ct)
if mediaType != "application/json" {
    // 415 Unsupported Media Type
}

mime.ParseMediaType application/json; charset=utf-8 ko‘rinishidagi qiymatdan faqat application/json qismini ajratib oladi.

Bu tekshiruv qat’iylikdan tashqari xavfsizlik uchun ham foydali. Brauzer boshqa saytdan application/json turidagi so‘rovni oldindan ruxsat so‘ramasdan (CORS preflight’siz) yubora olmaydi. Content-Type qat’iy tekshirilsa, CSRF hujumlarining bir turi to‘silib qoladi. Bu haqda CSRF & XSS darsida batafsil gaplashamiz.

Tayyor decodeJSON funksiyasi

Barcha qismlarni birlashtiramiz:

main.go
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "log"
    "mime"
    "net/http"
    "strings"
)

const maxBodyBytes = 1 << 20 // 1 MB

// RequestError — klientga ko‘rsatiladigan so‘rov xatosi.
type RequestError struct {
    Status  int
    Message string
}

func (e *RequestError) Error() string { return e.Message }

func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error {
    mediaType, _, _ := mime.ParseMediaType(r.Header.Get("Content-Type"))
    if mediaType != "application/json" {
        return &RequestError{http.StatusUnsupportedMediaType,
            "Content-Type application/json bo‘lishi kerak"}
    }

    r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
    dec := json.NewDecoder(r.Body)
    dec.DisallowUnknownFields()

    if err := dec.Decode(dst); err != nil {
        var syntaxErr *json.SyntaxError
        var typeErr *json.UnmarshalTypeError
        var maxErr *http.MaxBytesError

        switch {
        case errors.As(err, &syntaxErr):
            return &RequestError{http.StatusBadRequest,
                fmt.Sprintf("JSON %d-baytda noto‘g‘ri", syntaxErr.Offset)}

        case errors.Is(err, io.ErrUnexpectedEOF):
            return &RequestError{http.StatusBadRequest, "JSON to‘liq emas"}

        case errors.As(err, &typeErr):
            if typeErr.Field != "" {
                return &RequestError{http.StatusBadRequest,
                    fmt.Sprintf("%s maydonining turi noto‘g‘ri", typeErr.Field)}
            }
            return &RequestError{http.StatusBadRequest, "JSON obyekt kutilgan edi"}

        case errors.Is(err, io.EOF):
            return &RequestError{http.StatusBadRequest, "so‘rov tanasi bo‘sh"}

        case strings.HasPrefix(err.Error(), "json: unknown field "):
            field := strings.TrimPrefix(err.Error(), "json: unknown field ")
            return &RequestError{http.StatusBadRequest, "notanish maydon: " + field}

        case errors.As(err, &maxErr):
            return &RequestError{http.StatusRequestEntityTooLarge,
                fmt.Sprintf("tana %d baytdan oshmasligi kerak", maxErr.Limit)}

        default:
            return err
        }
    }

    if err := dec.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
        return &RequestError{http.StatusBadRequest,
            "tanada faqat bitta JSON obyekt bo‘lishi kerak"}
    }

    return nil
}

// ---------- Foydalanish ----------

type BookInput struct {
    Title  string `json:"title"`
    Author string `json:"author"`
    Year   int    `json:"year"`
}

func createBook(w http.ResponseWriter, r *http.Request) {
    var in BookInput
    if err := decodeJSON(w, r, &in); err != nil {
        var reqErr *RequestError
        if errors.As(err, &reqErr) {
            http.Error(w, reqErr.Message, reqErr.Status)
            return
        }
        log.Printf("tanani o‘qishda kutilmagan xato: %v", err)
        http.Error(w, "ichki server xatosi", http.StatusInternalServerError)
        return
    }

    fmt.Fprintf(w, "Qabul qilindi: %+v\n", in)
}

func main() {
    http.HandleFunc("POST /books", createBook)
    log.Println("Server: http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Sinab ko‘rish

Qulaylik uchun curl buyrug‘ini qisqartiramiz:

URL=http://localhost:8080/books
CT="Content-Type: application/json"
curl -X POST $URL -H "$CT" -d '{"title":"Sarob","author":"Qahhor","year":1937}'
curl -X POST $URL -d '{"title":"Sarob"}'
curl -X POST $URL -H "$CT" -d '{"title":"Sarob",}'
curl -X POST $URL -H "$CT" -d '{"title":"Sarob"'
curl -X POST $URL -H "$CT" -d '{"title":"Sarob","year":"1937"}'
curl -X POST $URL -H "$CT" -d '["Sarob"]'
curl -X POST $URL -H "$CT" -d '{"titel":"Sarob"}'
curl -X POST $URL -H "$CT" -d '{"title":"A"}{"title":"B"}'
curl -X POST $URL -H "$CT"
Qabul qilindi: {Title:Sarob Author:Qahhor Year:1937}
Content-Type application/json bo‘lishi kerak
JSON 18-baytda noto‘g‘ri
JSON to‘liq emas
year maydonining turi noto‘g‘ri
JSON obyekt kutilgan edi
notanish maydon: "titel"
tanada faqat bitta JSON obyekt bo‘lishi kerak
so‘rov tanasi bo‘sh

Har bir xato holatida klient nima noto‘g‘ri ekanini aniq biladi. Go ichki tuzilishiga oid tafsilotlar esa tashqariga chiqmaydi.

Katta tanani tekshirish uchun 2 MB hajmli fayl yaratib yuboramiz:

python3 -c 'print("{\"title\":\"" + "a"*2_000_000 + "\"}")' > big.json
curl -X POST $URL -H "$CT" --data-binary @big.json
tana 1048576 baytdan oshmasligi kerak

Tekshiruvlar tartibi

decodeJSON faqat sintaktik tekshiruvni bajaradi: JSON to‘g‘rimi va turlar mosmi? Ma’noviy tekshiruv esa undan keyin keladi: title bo‘sh emasmi, year mantiqiy oraliqdami, email to‘g‘ri formatdami?

1. Content-Type va hajm      → decodeJSON
2. JSON sintaksisi va turlar → decodeJSON
3. Qiymatlar mazmuni         → validatsiya
4. Biznes qoidalari          → service

Uchinchi qadam uchun qo‘lda if yozish o‘rniga validator kutubxonasi ishlatiladi.

Xulosa

  • Tana hajmini doim http.MaxBytesReader bilan cheklang.
  • DisallowUnknownFields xato yozilgan maydonlarni aniqlashga yordam beradi.
  • Ikkinchi Decode chaqiruvi tanada ortiqcha ma’lumot yo‘qligini tekshiradi.
  • json.SyntaxError, json.UnmarshalTypeError va http.MaxBytesError xatolarini tushunarli xabarlarga aylantiring.
  • Content-Type qat’iy tekshirilsa, API aniqroq va xavfsizroq bo‘ladi.
  • Barcha qoidalarni bitta decodeJSON funksiyasida jamlang va barcha handlerlarda qayta ishlating.

Keyingi darsda javob tomoniga o‘tamiz va JSON javoblarni izchil formatda qaytarishni o‘rganamiz.