Avatar ·

Как правильно обработать JSON POST запрос в Go? Проблемы с декодированием

Всем привет! Я только начал изучать Go и пишу простое REST API. Столкнулся с проблемой при обработке JSON POST запроса. Мой код вроде бы выглядит логично, но сервер всегда возвращает пустые данные или ошибку. Вот мой код обработчика: ```go func CreateUserHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var user User err := json.NewDecoder(r.Body).Decode(&user) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } defer r.Body.Close() fmt.Printf("User received: %+v\n", user) w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(user) } ``` Структура User: ```go type User struct { Name string `json:"name"` Email string `json:"email"` Age int `json:"age"` } ``` Я отправляю запрос через curl: ```bash curl -X POST http://localhost:8080/users -H "Content-Type: application/json" -d '{"name":"John","email":"john@example.com","age":30}' ``` Но в ответ получаю либо пустой JSON `{}`, либо ошибку "unexpected end of JSON input". Я уже перепробовал разные варианты, но проблема остаётся. Может быть, я неправильно использую `json.Decoder`? Или нужно что-то дополнительно настраивать? Буду благодарен за подробное объяснение! <<>>

Основная проблема: тело запроса читается только один раз

Ваша ошибка связана с тем, что в Go тело HTTP-запроса (r.Body) — это io.ReadCloser, который можно прочитать только один раз. После того как json.Decoder прочитал данные, поток закрывается. Но в вашем коде есть ещё одна тонкость: вы используете defer r.Body.Close() после декодирования, что в целом правильно, но проблема может быть в другом месте.

Давайте разберём ваш код и исправим его шаг за шагом.

Правильный подход к обработке JSON POST запроса

Вот исправленная версия вашего обработчика с комментариями:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    // 1. Проверяем метод
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // 2. Проверяем Content-Type (опционально, но рекомендуется)
    if r.Header.Get("Content-Type") != "application/json" {
        http.Error(w, "Content-Type must be application/json", http.StatusUnsupportedMediaType)
        return
    }

    // 3. Ограничиваем размер тела запроса (защита от DoS-атак)
    r.Body = http.MaxBytesReader(w, r.Body, 1048576) // 1 MB

    // 4. Декодируем JSON
    var user User
    err := json.NewDecoder(r.Body).Decode(&user)
    if err != nil {
        // Логируем ошибку для отладки
        log.Printf("Error decoding JSON: %v", err)
        http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
        return
    }

    // 5. Закрываем тело запроса (defer сработает при возврате из функции)
    defer r.Body.Close()

    // 6. Валидация данных (пример)
    if user.Name == "" || user.Email == "" {
        http.Error(w, "Name and Email are required", http.StatusBadRequest)
        return
    }

    // 7. Успешный ответ
    fmt.Printf("User received: %+v\n", user)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    http.HandleFunc("/users", CreateUserHandler)
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Почему ваш код не работал?

  • Проблема с curl: Если вы используете Windows PowerShell, обратите внимание на кавычки. В PowerShell нужно экранировать или использовать одинарные кавычки иначе:
curl -X POST http://localhost:8080/users -H "Content-Type: application/json" -d '{\"name\":\"John\",\"email\":\"john@example.com\",\"age\":30}'

Или сохраните JSON в файл и используйте -d @file.json.

  • Пустой JSON в ответе: Если вы не устанавливаете Content-Type в ответе, клиент может неправильно интерпретировать данные. Всегда явно указывайте w.Header().Set("Content-Type", "application/json").
  • Ошибка "unexpected end of JSON input": Эта ошибка часто возникает, если тело запроса пустое или было уже прочитано. Убедитесь, что вы не читаете r.Body до вызова вашего обработчика (например, в middleware).

Дополнительные советы

  • Используйте структуры с тегами json: Вы уже это делаете, но убедитесь, что имена полей в JSON совпадают с тегами (регистр имеет значение).
  • Проверяйте на nil: Если поле в JSON отсутствует, оно получит нулевое значение для своего типа. Используйте указатели или дополнительную валидацию.
  • Логируйте ошибки: В production используйте структурированное логирование (например, log/slog).
  • Обработка ошибок: Всегда возвращайте понятные сообщения об ошибках, но не раскрывайте внутреннюю структуру сервера.

Теперь ваш код должен работать корректно. Если проблема сохраняется — проверьте, не перехватывает ли запрос другой middleware или не закрывается ли тело запроса раньше времени.

<<>>

Альтернативный подход: использование json.Unmarshal с ioutil.ReadAll

Если вы предпочитаете более явный контроль над чтением тела запроса, можно использовать комбинацию ioutil.ReadAll и json.Unmarshal. Этот подход полезен, когда вам нужно прочитать тело запроса для логирования или повторного использования.

func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // Читаем всё тело запроса
    body, err := ioutil.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read request body", http.StatusInternalServerError)
        return
    }
    defer r.Body.Close()

    // Логируем сырой JSON (полезно для отладки)
    log.Printf("Raw request body: %s", string(body))

    // Декодируем
    var user User
    if err := json.Unmarshal(body, &user); err != nil {
        http.Error(w, fmt.Sprintf("Invalid JSON: %v", err), http.StatusBadRequest)
        return
    }

    // Валидация и ответ
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

Когда использовать этот подход?

  • Когда нужно логировать или модифицировать тело запроса перед декодированием.
  • Когда вы используете один и тот же JSON для разных целей (например, валидация схемы + декодирование).
  • При работе с вложенными или сложными JSON структурами, где требуется предварительная обработка.

Недостатки

  • Чтение всего тела в память может быть проблематичным для очень больших запросов (мегабайты и более).
  • Дополнительный шаг по сравнению с прямым декодированием из потока.

Выбирайте подход в зависимости от ваших задач. Для большинства REST API с небольшими JSON запросами оба варианта работают одинаково хорошо.