Add server WriteJSON and WriteError response helpers
Eliminates repeated marshal-set-header-write boilerplate in handlers.
WriteError produces consistent {"error": "..."} JSON responses.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
29
server/respond.go
Normal file
29
server/respond.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WriteJSON encodes v as JSON and writes it to w with the given status code.
|
||||||
|
// It sets Content-Type to application/json.
|
||||||
|
func WriteJSON(w http.ResponseWriter, status int, v any) error {
|
||||||
|
b, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_, err = w.Write(b)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteError writes a JSON error response with the given status code and
|
||||||
|
// message. The response body is {"error": "<message>"}.
|
||||||
|
func WriteError(w http.ResponseWriter, status int, msg string) error {
|
||||||
|
return WriteJSON(w, status, errorBody{Error: msg})
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorBody struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
72
server/respond_test.go
Normal file
72
server/respond_test.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package server_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.codelab.vc/pkg/httpx/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriteJSON(t *testing.T) {
|
||||||
|
t.Run("writes JSON with status and content type", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
type resp struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
err := server.WriteJSON(w, 201, resp{ID: 1, Name: "Alice"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.Code != 201 {
|
||||||
|
t.Fatalf("got status %d, want %d", w.Code, 201)
|
||||||
|
}
|
||||||
|
if ct := w.Header().Get("Content-Type"); ct != "application/json" {
|
||||||
|
t.Fatalf("Content-Type = %q, want %q", ct, "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
var decoded resp
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &decoded); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if decoded.ID != 1 || decoded.Name != "Alice" {
|
||||||
|
t.Fatalf("got %+v, want {ID:1 Name:Alice}", decoded)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("returns error for unmarshalable input", func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
err := server.WriteJSON(w, 200, make(chan int))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error for channel type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteError(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
err := server.WriteError(w, 404, "not found")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.Code != 404 {
|
||||||
|
t.Fatalf("got status %d, want %d", w.Code, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatalf("unmarshal: %v", err)
|
||||||
|
}
|
||||||
|
if body.Error != "not found" {
|
||||||
|
t.Fatalf("error = %q, want %q", body.Error, "not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user