Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions internal/api/handlers/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,19 @@ type APIError struct {
Message string `json:"message"` // e.g. "channel not found"
}

// respond writes a JSON response with the given status code and data.
// Logs if encoding fails since the header is already written at that point.
// respond encodes before committing the status, so an unsupported value cannot
// turn a serialization error into an empty success response.
func respond(w http.ResponseWriter, status int, data any) {
body, err := json.Marshal(data)
if err != nil {
slog.Error("api: failed to encode response", "component", "api", "error", err)
status = http.StatusInternalServerError
body = []byte(`{"error":{"code":"internal_server_error","message":"Unable to return this response."}}`)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(data); err != nil {
slog.Error("api: failed to encode response", "component", "api", "error", err)
if _, err := w.Write(append(body, '\n')); err != nil {
slog.Error("api: failed to write response", "component", "api", "error", err)
}
}

Expand Down
20 changes: 20 additions & 0 deletions internal/api/handlers/responses_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,31 @@ package handlers

import (
"encoding/json"
"math"
"net/http"
"net/http/httptest"
"testing"
)

func TestRespond_EncodingFailureIsJSONError(t *testing.T) {
for name, value := range map[string]any{
"nonfinite signal": map[string]float64{"snr": math.NaN()},
"unsupported type": make(chan int),
} {
t.Run(name, func(t *testing.T) {
w := httptest.NewRecorder()
respond(w, http.StatusOK, value)
if w.Code != http.StatusInternalServerError || w.Header().Get("Content-Type") != "application/json" {
t.Fatalf("status %d, headers %v", w.Code, w.Header())
}
var body map[string]APIError
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil || body["error"].Code != "internal_server_error" {
t.Fatalf("invalid error response %q: %v", w.Body.String(), err)
}
})
}
}

func TestRespond_StatusAndContentType(t *testing.T) {
w := httptest.NewRecorder()
respond(w, http.StatusOK, map[string]string{"hello": "world"})
Expand Down
Loading