Request Validation & Error Responses
Request Validation Is a Security Boundary
HTTP request validation is not a formality. It is a trust boundary. Every unvalidated field is a potential source of data corruption, undefined behavior, or security vulnerability.
Improper validation leads to:
- Silent data loss
- Mass assignment vulnerabilities
- Denial-of-service attacks
- Schema drift
- Unexpected zero values
Real Production Failure: Silent JSON Field Drop
A service accepted JSON payloads for user updates. A frontend typo changed a field name. The Go server ignored unknown fields silently and stored incomplete data.
No error was returned. The bug propagated to analytics and billing systems.
Root cause: default JSON decoder ignores unknown fields.
Default JSON Decoding Is Unsafe
json.NewDecoder(r.Body).Decode(&payload)
This allows unknown fields silently.
Strict JSON Decoding
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&payload); err != nil {
http.Error(w, "invalid payload", 400)
return
}
DisallowUnknownFields prevents schema drift.
Limit Request Body Size
Without limits, attackers can send massive payloads.
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
This limits body to 1MB.
Without this, memory exhaustion attacks are trivial.
Check for Multiple JSON Objects
Ensure only one object is decoded.
if decoder.More() {
http.Error(w, "multiple JSON objects not allowed", 400)
return
}
Transport vs Domain Validation
Transport Validation
- Required fields present
- Field types correct
- JSON structure valid
Domain Validation
- Business rules
- Cross-field constraints
- Permission checks
Keep these layers separate.
Manual Validation Example
if payload.Email == "" {
return errors.New("email required")
}
if len(payload.Password) < 8 {
return errors.New("password too short")
}
Return structured errors when possible.
Numeric Overflow Risk
JSON numbers decode into float64 by default.
var data map[string]interface{}
json.Unmarshal(body, &data)
This loses integer precision for large values.
Prefer typed structs.
Streaming vs Buffering
Decoder streams input. Avoid reading full body into memory unnecessarily.
However, if you need to log raw payload, buffer carefully and limit size.
Content-Type Validation
if r.Header.Get("Content-Type") != "application/json" {
http.Error(w, "unsupported content type", 415)
return
}
Reject unexpected content types early.
Mass Assignment Risk
If struct contains sensitive fields, JSON binding may populate them unexpectedly.
type User struct {
Email string
IsAdmin bool
}
If IsAdmin is not validated, client may escalate privileges.
Separate input DTO from domain model.
Validation Libraries
Libraries like go-playground/validator help enforce struct tags.
type CreateUser struct {
Email string `validate:"required,email"`
}
But understand validation logic — do not blindly trust tags.
Handling Partial Updates
PATCH endpoints require careful validation.
Distinguish between zero value and omitted field using pointers.
type UpdateUser struct {
Email *string
}
DOS Vectors
- Large JSON payloads
- Deeply nested JSON objects
- Slow request body streaming
- Malformed Unicode
Combine ReadTimeout and MaxBytesReader.
Error Response Structure
Return structured validation errors:
{
"error": "validation_failed",
"fields": {
"email": "invalid format"
}
}
Do not leak internal error details.
Testing Validation
req := httptest.NewRequest("POST", "/users", bytes.NewReader(invalidJSON))
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != 400 {
t.Fatal("expected validation failure")
}
Test negative cases aggressively.
Common Anti-Patterns
- Ignoring unknown fields
- No body size limit
- Binding directly into domain structs
- Trusting client-provided IDs
- Logging raw sensitive payloads
Operational Checklist
- Body size limited
- Unknown fields rejected
- DTO separated from domain model
- Content-Type validated
- Numeric types defined explicitly
- Validation tested with invalid inputs
- Structured error responses implemented
Final Perspective
Request validation is the front door of your service. Silent acceptance of malformed input leads to cascading failures and security exposure. Strict decoding, size limits, and layered validation transform your HTTP layer into a reliable, defensive boundary suitable for production environments.