From 2139121683ecd58a01ea5c88bf32b9ee613c7ae3 Mon Sep 17 00:00:00 2001 From: Panagiotis Siatras Date: Thu, 4 May 2023 22:16:12 +0300 Subject: [PATCH] optimized render.JSON (#929) * api/render: render JSON directly to the underlying writer * also consider json.MarshalerError a panic --- api/render/render.go | 24 +++++++++++++++------- api/render/render_test.go | 43 +++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/api/render/render.go b/api/render/render.go index 81a7a02e..7829ba25 100644 --- a/api/render/render.go +++ b/api/render/render.go @@ -2,7 +2,6 @@ package render import ( - "bytes" "encoding/json" "errors" "net/http" @@ -24,14 +23,25 @@ func JSON(w http.ResponseWriter, v interface{}) { // JSONStatus sets the Content-Type of w to application/json unless one is // specified. func JSONStatus(w http.ResponseWriter, v interface{}, status int) { - var b bytes.Buffer - if err := json.NewEncoder(&b).Encode(v); err != nil { - panic(err) - } - setContentTypeUnlessPresent(w, "application/json") w.WriteHeader(status) - _, _ = b.WriteTo(w) + + if err := json.NewEncoder(w).Encode(v); err != nil { + var errUnsupportedType *json.UnsupportedTypeError + if errors.As(err, &errUnsupportedType) { + panic(err) + } + + var errUnsupportedValue *json.UnsupportedValueError + if errors.As(err, &errUnsupportedValue) { + panic(err) + } + + var errMarshalError *json.MarshalerError + if errors.As(err, &errMarshalError) { + panic(err) + } + } log.EnabledResponse(w, v) } diff --git a/api/render/render_test.go b/api/render/render_test.go index 06d092d3..e88544c7 100644 --- a/api/render/render_test.go +++ b/api/render/render_test.go @@ -1,8 +1,10 @@ package render import ( + "encoding/json" "fmt" "io" + "math" "net/http" "net/http/httptest" "strconv" @@ -26,10 +28,43 @@ func TestJSON(t *testing.T) { assert.Empty(t, rw.Fields()) } -func TestJSONPanics(t *testing.T) { - assert.Panics(t, func() { - JSON(httptest.NewRecorder(), make(chan struct{})) - }) +func TestJSONPanicsOnUnsupportedType(t *testing.T) { + jsonPanicTest[json.UnsupportedTypeError](t, make(chan struct{})) +} + +func TestJSONPanicsOnUnsupportedValue(t *testing.T) { + jsonPanicTest[json.UnsupportedValueError](t, math.NaN()) +} + +func TestJSONPanicsOnMarshalerError(t *testing.T) { + var v erroneousJSONMarshaler + jsonPanicTest[json.MarshalerError](t, v) +} + +type erroneousJSONMarshaler struct{} + +func (erroneousJSONMarshaler) MarshalJSON() ([]byte, error) { + return nil, assert.AnError +} + +func jsonPanicTest[T json.UnsupportedTypeError | json.UnsupportedValueError | json.MarshalerError](t *testing.T, v any) { + t.Helper() + + defer func() { + var err error + if r := recover(); r == nil { + t.Fatal("expected panic") + } else if e, ok := r.(error); !ok { + t.Fatalf("did not panic with an error (%T)", r) + } else { + err = e + } + + var e *T + assert.ErrorAs(t, err, &e) + }() + + JSON(httptest.NewRecorder(), v) } type renderableError struct {