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:
2026-06-24 23:42:57 +01:00
co-authored by Sisyphus
parent 08c8828bb0
commit da0e64c02b
3 changed files with 130 additions and 0 deletions
+13
View File
@@ -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)
})
}
+96
View File
@@ -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())
}
}
+21
View File
@@ -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})
}