Fungsi c.BodyParser() di Go Fiber secara default mendekompresi payload JSON secara permisif. Jika klien mengirimkan atribut yang tidak didefinisikan di struct target—misalnya karena salah ketik nama field—data tersebut diabaikan secara diam-diam (silent drop). Backend menerima nilai kosong atau zero-value tanpa menghasilkan pesan error apa pun.
Risiko Silent Drop terhadap Integritas Kontrak API
Perilaku default parser membawa risiko fatal pada arsitektur sistem berbasis kontrak (schema-first atau API-contract first):
- Kerusakan Nilai Bisnis: Klien mengirimkan
{"acount_id": "12345"}(salah ketik). Struct Go dengan fieldAccountIDtetap terisi string kosong (""). Proses transaksi berjalan tanpa identitas rekening yang valid. - Bypass Logika Mutasi: Pada endpoint update (
PATCH/PUT), field yang diabaikan menyebabkan sistem menganggap klien tidak berniat memperbarui atribut tersebut, padahal terjadi ketidakcocokan versi SDK klien dan server. - Masalah Audit dan Keamanan: Payload yang membawa properti tambahan ilegal tidak terdokumentasi dan lolos masuk ke lapisan logika aplikasi.
Custom Strict JSON Parser Menggunakan DisallowUnknownFields
Untuk memvalidasi field yang tidak dikenali, gunakan json.NewDecoder dari paket standar Go encoding/json yang digabungkan dengan method DisallowUnknownFields(). Baca byte buffer mentah via c.Body() menggunakan bytes.NewReader.
Pendekatan ini memisahkan parsing ketat dari binder default Fiber:
package parser
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"strings"
"github.com/gofiber/fiber/v2"
)
func ParseStrictJSON(c *fiber.Ctx, out interface{}) error {
body := c.Body()
if len(body) == 0 {
return errors.New("body payload cannot be empty")
}
reader := bytes.NewReader(body)
decoder := json.NewDecoder(reader)
decoder.DisallowUnknownFields()
if err := decoder.Decode(out); err != nil {
var syntaxErr *json.SyntaxError
var unmarshalTypeErr *json.UnmarshalTypeError
switch {
case errors.As(err, &syntaxErr):
return fmt.Errorf("malformed json at offset %d: %w", syntaxErr.Offset, err)
case errors.As(err, &unmarshalTypeErr):
return fmt.Errorf("invalid type for field '%s' at offset %d", unmarshalTypeErr.Field, unmarshalTypeErr.Offset)
case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return fmt.Errorf("unmapped field rejected: %s", fieldName)
case errors.Is(err, io.EOF):
return errors.New("request body is empty")
default:
return err
}
}
// Pastikan tidak ada data JSON ganda di belakang payload
if decoder.More() {
return errors.New("body must only contain a single JSON object")
}
return nil
}Integrasi dengan go-playground/validator/v10
Parsing skema yang ketat hanya memvalidasi bentuk representasi JSON. Untuk memverifikasi nilai wajib (required) dan format bisnis, gabungkan custom parser dengan validator/v10.
package main
import (
"net/http"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
)
var validate = validator.New()
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=4,max=32"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=0,lte=130"`
}
func UserHandler(c *fiber.Ctx) error {
var req CreateUserRequest
// 1. Strict JSON Decode
if err := ParseStrictJSON(c, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Bad Request",
"detail": err.Error(),
})
}
// 2. Struct Validation
if err := validate.Struct(req); err != nil {
return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{
"error": "Validation Failed",
"detail": err.Error(),
})
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"status": "success",
"data": req,
})
}Pengujian Otomatis Menggunakan app.Test
Verifikasi penolakan unknown field menggunakan test harness bawaan Fiber (app.Test) tanpa perlu menjalankan port jaringan eksternal.
package main
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/gofiber/fiber/v2"
)
func setupTestApp() *fiber.App {
app := fiber.New()
app.Post("/users", UserHandler)
return app
}
func TestStrictJSONParser_RejectsUnknownField(t *testing.T) {
app := setupTestApp()
// Payload membawa atribut asing 'role' yang tidak ada pada struct
payload := []byte(`{
"username": "johndoe",
"email": "[email protected]",
"age": 28,
"role": "admin"
}`)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("failed to run test request: %v", err)
}
if resp.StatusCode != fiber.StatusBadRequest {
t.Errorf("expected status %d, got %d", fiber.StatusBadRequest, resp.StatusCode)
}
}
func TestStrictJSONParser_AcceptsValidPayload(t *testing.T) {
app := setupTestApp()
payload := []byte(`{
"username": "johndoe",
"email": "[email protected]",
"age": 28
}`)
req := httptest.NewRequest(http.MethodPost, "/users", bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
resp, err := app.Test(req, -1)
if err != nil {
t.Fatalf("failed to run test request: %v", err)
}
if resp.StatusCode != fiber.StatusCreated {
t.Errorf("expected status %d, got %d", fiber.StatusCreated, resp.StatusCode)
}
}Trade-off dan Pertimbangan Performa
Menerapkan DisallowUnknownFields memiliki konsekuensi yang perlu diperhitungkan sebelum diterapkan ke seluruh endpoint:
- Evolusi Schema API: Klien yang memperbarui versi JSON sebelum backend diperbarui akan mengalami kegagalan request. Backward compatibility menjadi lebih kaku.
- Alokasi Memori: Penggunaan
c.Body()membaca seluruh buffer ke memori RAM, berbeda dengan streaming langsung dari soket TCP. Untuk payload berukuran besar (di atas beberapa megabyte), batasi limit request body menggunakan middlewarelimiter.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!