feat(mw): add content-type and response middleware
Add Content-Type enforcement middleware and generic JSON response helpers to standardize API responses. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package mw
|
||||
|
||||
import "net/http"
|
||||
|
||||
// JsonContentType sets Content-Type: application/json on all API responses.
|
||||
// Individual handlers that need to override (e.g. binary/image responses)
|
||||
// should set their own Content-Type header after this middleware runs.
|
||||
func JsonContentType(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
package mw
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestJsonContentType_Middleware verifies that the JsonContentType middleware
|
||||
// sets Content-Type: application/json on all responses.
|
||||
func TestJsonContentType_Middleware(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create a simple handler that writes "ok"
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
// Wrap with JsonContentType middleware
|
||||
handler := JsonContentType(next)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
// Verify Content-Type is set
|
||||
ct := w.Header().Get("Content-Type")
|
||||
if ct != "application/json" {
|
||||
t.Errorf("expected Content-Type 'application/json', got %q", ct)
|
||||
}
|
||||
|
||||
// Verify body is preserved
|
||||
if w.Body.String() != "ok" {
|
||||
t.Errorf("expected body 'ok', got %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestJsonContentType_HandlerCanOverride verifies that the downstream handler
|
||||
// can override the Content-Type if needed.
|
||||
func TestJsonContentType_HandlerCanOverride(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write([]byte("override"))
|
||||
})
|
||||
|
||||
handler := JsonContentType(next)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
// The handler's Content-Type should win (it was set after middleware)
|
||||
ct := w.Header().Get("Content-Type")
|
||||
if ct != "text/plain; charset=utf-8" {
|
||||
t.Errorf("expected Content-Type 'text/plain; charset=utf-8', got %q", ct)
|
||||
}
|
||||
|
||||
if w.Body.String() != "override" {
|
||||
t.Errorf("expected body 'override', got %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestJsonContentType_StatusOK verifies that the middleware passes through the
|
||||
// handler's status code correctly.
|
||||
func TestJsonContentType_StatusOK(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(`{"id": "abc"}`))
|
||||
})
|
||||
|
||||
handler := JsonContentType(next)
|
||||
|
||||
req := httptest.NewRequest("POST", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Errorf("expected status 201, got %d", w.Code)
|
||||
}
|
||||
if w.Header().Get("Content-Type") != "application/json" {
|
||||
t.Errorf("expected Content-Type 'application/json', got %q", w.Header().Get("Content-Type"))
|
||||
}
|
||||
if w.Body.String() != `{"id": "abc"}` {
|
||||
t.Errorf("expected body `{\"id\": \"abc\"}`, got %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package mw
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// RespondJSON writes a JSON response with the given status code.
|
||||
// The Content-Type is always set to application/json, overriding any
|
||||
// middleware-set value, to ensure consistent JSON error responses.
|
||||
func RespondJSON(w http.ResponseWriter, status int, data any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
// RespondError writes a JSON error response with the given status code and message.
|
||||
// Use instead of http.Error() to ensure error responses are application/json.
|
||||
func RespondError(w http.ResponseWriter, status int, message string) {
|
||||
RespondJSON(w, status, map[string]string{"error": message})
|
||||
}
|
||||
Reference in New Issue
Block a user