Skip to content

Commit 8e41156

Browse files
committed
use writer that can delay sending status code
1 parent 6b2ea47 commit 8e41156

File tree

3 files changed

+58
-10
lines changed

3 files changed

+58
-10
lines changed

context.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,15 @@ func (c *Context) Render(code int, name string, data any) (err error) {
415415
if c.echo.Renderer == nil {
416416
return ErrRendererNotRegistered
417417
}
418+
// as Renderer.Render can fail, and in that case we need to delay sending status code to the client until
419+
// (global) error handler decides the correct status code for the error to be sent to the client, so we need to write
420+
// the rendered template to the buffer first.
421+
//
422+
// html.Template.ExecuteTemplate() documentations writes:
423+
// > If an error occurs executing the template or writing its output,
424+
// > execution stops, but partial results may already have been written to
425+
// > the output writer.
426+
418427
buf := new(bytes.Buffer)
419428
if err = c.echo.Renderer.Render(c, buf, name, data); err != nil {
420429
return
@@ -455,14 +464,12 @@ func (c *Context) jsonPBlob(code int, callback string, i any) (err error) {
455464
func (c *Context) json(code int, i any, indent string) error {
456465
c.writeContentType(MIMEApplicationJSON)
457466

458-
if r, err := UnwrapResponse(c.response); err == nil {
459-
// *echo.Response can delay sending status code until the first Write is called. As serialization can fail, we should delay
460-
// sending the status code to the client until serialization is complete (first Write would be an indication it succeeded)
461-
// Unsuccessful serialization error needs to go through the error handler and get a proper status code there.
462-
r.Status = code
463-
} else {
464-
return fmt.Errorf("json: response does not unwrap to *echo.Response")
465-
}
467+
// as JSONSerializer.Serialize can fail, and in that case we need to delay sending status code to the client until
468+
// (global) error handler decides correct status code for the error to be sent to the client.
469+
// For that we need to use writer that can store the proposed status code until the first Write is called.
470+
resp := c.Response()
471+
c.SetResponse(&delayedStatusWriter{ResponseWriter: resp, status: code})
472+
defer c.SetResponse(resp)
466473

467474
return c.echo.JSONSerializer.Serialize(c, i, indent)
468475
}

context_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io"
1313
"io/fs"
1414
"log/slog"
15+
"math"
1516
"mime/multipart"
1617
"net/http"
1718
"net/http/httptest"
@@ -251,8 +252,8 @@ func TestContextJSONWithNotEchoResponse(t *testing.T) {
251252

252253
c.SetResponse(rec)
253254

254-
err := c.JSON(http.StatusOK, map[string]interface{}{"foo": "bar"})
255-
assert.EqualError(t, err, "json: response does not unwrap to *echo.Response")
255+
err := c.JSON(http.StatusCreated, map[string]float64{"foo": math.NaN()})
256+
assert.EqualError(t, err, "json: unsupported value: NaN")
256257

257258
assert.Equal(t, http.StatusOK, rec.Code) // status code must not be sent to the client
258259
assert.Empty(t, rec.Body.String()) // body must not be sent to the client

response.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,3 +130,43 @@ func UnwrapResponse(rw http.ResponseWriter) (*Response, error) {
130130
}
131131
}
132132
}
133+
134+
// delayedStatusWriter is a wrapper around http.ResponseWriter that delays writing the status code until first Write is called.
135+
// This allows (global) error handler to decide correct status code to be sent to the client.
136+
type delayedStatusWriter struct {
137+
http.ResponseWriter
138+
commited bool
139+
status int
140+
}
141+
142+
func (w *delayedStatusWriter) WriteHeader(statusCode int) {
143+
// in case something else writes status code explicitly before us we need mark response commited
144+
w.commited = true
145+
w.ResponseWriter.WriteHeader(statusCode)
146+
}
147+
148+
func (w *delayedStatusWriter) Write(data []byte) (int, error) {
149+
if !w.commited {
150+
w.commited = true
151+
if w.status == 0 {
152+
w.status = http.StatusOK
153+
}
154+
w.ResponseWriter.WriteHeader(w.status)
155+
}
156+
return w.ResponseWriter.Write(data)
157+
}
158+
159+
func (w *delayedStatusWriter) Flush() {
160+
err := http.NewResponseController(w.ResponseWriter).Flush()
161+
if err != nil && errors.Is(err, http.ErrNotSupported) {
162+
panic(errors.New("response writer flushing is not supported"))
163+
}
164+
}
165+
166+
func (w *delayedStatusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
167+
return http.NewResponseController(w.ResponseWriter).Hijack()
168+
}
169+
170+
func (w *delayedStatusWriter) Unwrap() http.ResponseWriter {
171+
return w.ResponseWriter
172+
}

0 commit comments

Comments
 (0)