Quick Reference
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}ReadHeaderTimeout은 느린 header, ReadTimeout은 요청 전체 읽기, WriteTimeout은 응답 쓰기, IdleTimeout은 다음 keep-alive 요청 대기를 제한합니다. body 크기는 별도로 handler 입구에서 제한합니다.
timeout 경계
ReadHeaderTimeout은 느린 header 전송을 막는다
ReadHeaderTimeout은 서버가 요청 header를 읽는 데 허용할 시간을 제한합니다. header를 다 읽으면 read deadline이 다시 설정되어 handler가 body 속도 정책을 정할 수 있습니다. 값이 0이면 ReadTimeout을 사용하고, 둘 다 0 이하이면 header 제한이 없습니다.
느린 클라이언트가 연결만 잡고 header를 천천히 보내면 connection과 goroutine이 오래 묶일 수 있습니다. 대부분의 API는 ReadHeaderTimeout을 먼저 두고, 파일 업로드처럼 body 특성이 다른 endpoint는 handler에서 별도 정책을 둡니다.
ReadTimeout과 WriteTimeout은 요청 전체와 응답 쓰기 경계다
ReadTimeout은 header와 body를 포함해 요청 전체를 읽는 최대 시간입니다. 이 값은 handler가 요청별로 다르게 정할 수 없으므로, 업로드 속도와 크기가 다른 endpoint를 한 서버에 섞을 때는 지나치게 짧은 공통값이 정상 업로드를 끊을 수 있습니다.
WriteTimeout은 요청 header를 읽은 뒤 응답 write가 timeout 되기 전까지의 최대 시간이며, 새 요청 header를 읽을 때 다시 설정됩니다. 이는 handler 실행 시간을 요청별로 제어하는 도구가 아닙니다. streaming, SSE, 긴 polling은 공통 WriteTimeout과 맞지 않을 수 있으므로 전용 endpoint 또는 별도 서버 정책을 검토합니다.
func upload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
// decode multipart or JSON here
}IdleTimeout과 MaxHeaderBytes도 별도 경계다
IdleTimeout은 keep-alive connection에서 다음 요청을 기다리는 최대 시간입니다. 0이면 ReadTimeout 값을 사용하며, 둘 다 0 이하이면 idle 연결을 시간으로 끊지 않습니다. MaxHeaderBytes는 request line과 header key/value를 읽는 최대 바이트 수이며 body 크기는 제한하지 않습니다. 값이 0이면 표준 라이브러리 기본값을 씁니다.
body 크기와 오류 처리
body 크기는 읽기 전에 handler에서 제한한다
http.MaxBytesReader는 들어오는 request body 크기를 제한하는 데 쓰입니다. reverse proxy나 load balancer에서 body size를 막아도, 애플리케이션 handler에서도 한 번 더 제한하는 편이 안전합니다. 특히 json.Decoder, io.ReadAll, multipart parsing처럼 body를 읽는 코드 앞에 제한을 두어야 큰 body가 메모리와 CPU를 과도하게 쓰는 일을 줄일 수 있습니다.
서버 request body는 net/http 서버가 닫습니다. 이 예제처럼 r.Body를 MaxBytesReader로 바꿔도 handler가 defer r.Body.Close()를 추가할 필요는 없습니다.
func decodeCreate(w http.ResponseWriter, r *http.Request) (*CreateInput, error) {
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
var input CreateInput
err := json.NewDecoder(r.Body).Decode(&input)
return &input, err
}
func create(w http.ResponseWriter, r *http.Request) {
input, err := decodeCreate(w, r)
if err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
_ = input
}MaxBytesReader가 한도를 넘으면 읽기에서 *http.MaxBytesError가 나옵니다. 이를 다른 JSON 오류와 같은 400으로 뭉개지 말고 413 Payload Too Large로 구분합니다. decode 함수는 parse error만 반환하고, handler가 HTTP status와 response를 한 번만 쓰게 하면 호출 규약이 분명해집니다.
ParseMultipartForm(maxMemory)의 maxMemory는 file part를 메모리에 둘 양일 뿐 전체 body 상한이 아닙니다. multipart도 먼저 MaxBytesReader로 전체 크기를 제한합니다.
endpoint별 정책
| 제한 대상 | Go 표면 |
|---|---|
| header를 천천히 보내는 요청 | ReadHeaderTimeout |
| header와 body 읽기 전체 | ReadTimeout |
| 응답 쓰기 지연 | WriteTimeout |
| keep-alive idle connection | IdleTimeout |
| header 크기 | MaxHeaderBytes |
| request body 크기 | http.MaxBytesReader |
작은 JSON API는 짧은 header timeout, 명시적인 body limit, 예측 가능한 read/write timeout을 함께 둡니다. 업로드는 body 한도와 업로드 속도 정책을 따로 정하고, 응답 streaming은 write deadline이 정상 연결을 끊지 않는지 별도 검증합니다. 모든 handler에 같은 body 상한이 맞는 경우에는 http.MaxBytesHandler로 공통 handler를 감쌀 수도 있습니다.
주의할 점
timeout을 전부 같은 값으로 맞추면 endpoint 성격을 반영하기 어렵습니다. ReadTimeout은 body별로 조절할 수 없고 WriteTimeout은 handler 내부 작업을 취소하는 수단이 아닙니다. 작은 JSON API, 파일 업로드, streaming 응답은 공통 timeout과 handler 단위 body 제한을 분리해 설계합니다.
// 위험: body 제한 없이 전체를 메모리에 읽음
body, err := io.ReadAll(r.Body)
if err != nil {
return
}
_ = body큰 요청을 받을 수 있는 handler라면 ReadAll이나 decoder 호출 전에 body size limit을 먼저 둡니다.
참고 링크
1 sources