B.27. Request Body Size Limit
Tanpa adanya limit ukuran payload, endpoint yang menerima payload (JSON, XML, file upload) akan rentan terhadap serangan memory exhaustion di mana client mengirimkan data berukuran sangat besar sehingga server kehabisan memori. Go menyediakan http.MaxBytesReader untuk antisipasi masalah ini.
B.27.1. Fungsi http.MaxBytesReader()
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
Fungsi http.MaxBytesReader() membungkus r.Body dengan reader yang berhenti membaca setelah jumlah byte yang ditentukan tercapai, kemudian mengembalikan error bertipe *http.MaxBytesError.
Parameter:
w:http.ResponseWriter, digunakan untuk menandai response sebagai error secara internalr.Body: body request aslimaxBytes: batas ukuran dalam byte
Pastikan untuk memanggil
http.MaxBytesReadersebelum membaca body, bukan setelahnya.
B.27.2. Implementasi pada JSON Payload
Pada kode berikut, fungsi handleUpload() merupakan HTTP handler untuk parsing payload.
const maxBodyBytes = 1 << 20 // 1 MB
func handleUpload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
var payload map[string]any
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"received": payload})
}
Konstanta maxBodyBytes bernilai 1 << 20 yang nilainya adalah 1.048.576 atau setara 1 MB. Cara ini umum digunakan untuk ukuran berbasis power-of-two karena lebih eksplisit dibanding menulis 1048576 secara harfiah.
Di baris pertama handler, r.Body di-assign ulang dengan hasil http.MaxBytesReader() agar nantinya saat pembacaan payload limit maxBodyBytes diberlakukan sebagai batas. Perlu diperhatikan bahwa MaxBytesReader mengembalikan reader baru yang membungkus body asli, bukan memodifikasinya secara in-place, sehingga hasilnya harus di-assign kembali ke r.Body. Jika dilewatkan, limit tidak akan berlaku.
Statement json.NewDecoder(r.Body).Decode() bisa gagal karena dua hal:
- Body terlalu besar
- JSON tidak valid
Fungsi errors.As() digunakan untuk memeriksa apakah error bertipe *http.MaxBytesError atau bukan, sehingga kedua kondisi tersebut bisa dikembalikan dengan status code yang tepat: 413 untuk payload terlalu besar, 400 untuk JSON rusak.
B.27.3. Implementasi pada File Upload
Masih sama seperti pada kode sebelumnya (handleUpload()), http.MaxBytesReader() diaplikasikan sebelum pembacaan payload (yang pada kode berikut adalah operasi ParseMultipartForm).
func handleFileUpload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
if err := r.ParseMultipartForm(maxBodyBytes); err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
http.Error(w, "file too large (max 1MB)", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "failed to parse form", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "file is required", http.StatusBadRequest)
return
}
defer file.Close()
data, _ := io.ReadAll(file)
log.Printf("received file: %s, size: %d bytes", header.Filename, len(data))
w.Write([]byte("received: " + header.Filename))
}
Pola yang digunakan di sini sama seperti pada handleUpload(). Satu hal yang perlu dicatat: parameter maxMemory pada ParseMultipartForm bukan limit ukuran payload, melainkan hanya mengontrol seberapa besar data form disimpan di memori sebelum ditulis ke file temporary. Itulah mengapa MaxBytesReader tetap dibutuhkan.
r.FormFile("file") mengambil file yang dikirim via field bernama "file". Fungsi ini mengembalikan tiga nilai: multipart.File (isi file yang bisa dibaca seperti io.Reader), *multipart.FileHeader (metadata seperti nama file dan ukuran), dan error jika field tidak ada atau bukan file.
io.ReadAll(file) membaca seluruh isi file ke dalam slice byte. Di sini hasilnya hanya digunakan untuk mengambil ukuran file via len(data) yang kemudian di-log. Pada aplikasi nyata, data bisa diteruskan ke proses berikutnya seperti menyimpan ke disk atau object storage.
B.27.4. Implementasi Lengkap
Buat file main.go.
package main
import (
"encoding/json"
"errors"
"io"
"log"
"net/http"
)
const maxBodyBytes = 1 << 20 // 1 MB
func handleUpload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
var payload map[string]any
err := json.NewDecoder(r.Body).Decode(&payload)
if err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
http.Error(w, "request body too large", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]any{"received": payload})
}
func handleFileUpload(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
if err := r.ParseMultipartForm(maxBodyBytes); err != nil {
var maxBytesErr *http.MaxBytesError
if errors.As(err, &maxBytesErr) {
http.Error(w, "file too large (max 1MB)", http.StatusRequestEntityTooLarge)
return
}
http.Error(w, "failed to parse form", http.StatusBadRequest)
return
}
file, header, err := r.FormFile("file")
if err != nil {
http.Error(w, "file is required", http.StatusBadRequest)
return
}
defer file.Close()
data, _ := io.ReadAll(file)
log.Printf("received file: %s, size: %d bytes", header.Filename, len(data))
w.Write([]byte("received: " + header.Filename))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("POST /upload/json", handleUpload)
mux.HandleFunc("POST /upload/file", handleFileUpload)
log.Println("server started at localhost:9000")
err := http.ListenAndServe(":9000", mux)
if err != nil {
log.Fatal(err)
}
}
B.27.5. Testing
Jalankan server lalu coba keempat skenario berikut.
# Payload dengan ukuran normal
curl -X POST http://localhost:9000/upload/json \
-H "Content-Type: application/json" \
-d '{"name":"batman"}'
Response: {"received":{"name":"batman"}} dengan status 200.
# Payload dengan ukuran melebihi limit (generate 2MB data lalu pipe ke curl)
dd if=/dev/zero bs=1024 count=2048 | curl -X POST http://localhost:9000/upload/json \
-H "Content-Type: application/json" \
--data-binary @-
Response: request body too large dengan status 413 Request Entity Too Large.
# Upload file dengan ukuran normal
echo "hello world" > test.txt
curl -X POST http://localhost:9000/upload/file \
-F "[email protected]"
Response: received: test.txt dengan status 200.
# Upload file dengan ukuran melebihi limit (generate file 2MB)
dd if=/dev/zero of=bigfile.bin bs=1024 count=2048
curl -X POST http://localhost:9000/upload/file \
-F "[email protected]"
Response: file too large (max 1MB) dengan status 413 Request Entity Too Large.