Quick Syntax
func createUser(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var req CreateUserRequest
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
var maxErr *http.MaxBytesError
if errors.As(err, &maxErr) {
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
if err := dec.Decode(&struct{}{}); err != io.EOF {
http.Error(w, "JSON body must contain one value", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(CreateUserResponse{ID: "u_123"})
}handler 흐름
request body는 제한한 뒤 decode한다
JSON handler에서 가장 먼저 볼 것은 body 크기입니다. json.NewDecoder(r.Body)를 바로 호출하기 전에 http.MaxBytesReader로 크기 상한을 두면 큰 body로 인한 자원 낭비를 줄일 수 있습니다. server request의 body는 서버가 닫으므로 일반 handler가 defer r.Body.Close()를 둘 필요는 없습니다.
MaxBytesReader 초과는 *http.MaxBytesError로 구분해 413 Payload Too Large를 돌리고, 문법·타입 오류는 400 Bad Request로 분리합니다. 첫 Decode만 하면 {} 뒤에 다른 JSON 값이 더 붙은 body를 허용할 수 있으므로 두 번째 decode가 io.EOF인지도 확인합니다.
JSON API라면 Content-Type도 정책으로 정합니다. mime.ParseMediaType로 parameter를 떼어낸 뒤 application/json인지 확인하면 application/json; charset=utf-8은 허용하면서 다른 본문을 415 Unsupported Media Type으로 돌려줄 수 있습니다. JSON parse 성공은 도메인 validation 성공이 아니므로 필수값·범위·권한 검사는 decode 뒤에 별도로 둡니다.
DisallowUnknownFields는 엄격한 API 계약에 맞다
encoding/json decoder는 기본적으로 struct에 없는 JSON object key를 무시합니다. public API에서 오타를 빨리 잡고 싶거나, 클라이언트가 잘못된 field를 보내면 실패해야 하는 계약이라면 DisallowUnknownFields()를 켤 수 있습니다. 반대로 forward compatibility를 중요하게 보고 unknown field를 무시하는 정책이라면 기본 동작이 더 맞을 수 있습니다.
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
if err := dec.Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}엄격함을 선택하면 클라이언트 배포 순서와 API versioning까지 같이 고려해야 합니다.
response는 header, status, body 순서로 쓴다
Go에서는 response body를 쓰기 전에 header와 status code를 먼저 정해야 합니다. json.NewEncoder(w).Encode(...)가 body를 쓰기 시작하면 status code는 기본 200 OK로 확정될 수 있습니다. 생성 성공이면 WriteHeader(http.StatusCreated)를 먼저 호출하고, 그 다음 JSON body를 씁니다.
동적 payload라서 encode가 실패할 수 있으면 json.Marshal을 먼저 실행해 오류를 response commit 전에 처리하거나, streaming response라면 encode 오류를 바꿀 수 없는 사후 로그로 남깁니다. Encode 오류 뒤에 다른 HTTP status를 쓰는 코드는 이미 응답이 시작됐을 수 있습니다.
선택 기준
| 상황 | 먼저 볼 것 |
|---|---|
| request body가 큰지 제한 | http.MaxBytesReader |
| JSON field 오타를 실패로 처리 | DisallowUnknownFields |
| backward/forward compatibility 우선 | unknown field 허용 |
| JSON 응답 | Content-Type: application/json |
| 생성 성공 | 201 Created 후 encode |
| decode 실패 | 400 Bad Request |
주의할 점
JSON response를 쓰기 시작한 뒤에는 status code를 바꾸기 어렵습니다. 응답 header와 status를 먼저 확정하고 그 다음 encoder를 호출하는 순서를 유지해야 합니다.
// 위험: Encode가 먼저 body를 쓰면 status가 200으로 확정될 수 있음
json.NewEncoder(w).Encode(resp)
w.WriteHeader(http.StatusCreated)status code가 중요한 handler에서는 WriteHeader를 body write보다 먼저 호출합니다.
참고 링크
2 sources