Как правильно обработать JSON POST запрос в Go? Проблемы с декодированием
Основная проблема: тело запроса читается только один раз
Ваша ошибка связана с тем, что в 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 запросами оба варианта работают одинаково хорошо.