Skip to content
Open
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
15 changes: 12 additions & 3 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ func (c *Context) Response() http.ResponseWriter {
return c.response
}

// SetResponse sets `*http.ResponseWriter`. Some middleware require that given ResponseWriter implements following
// method `Unwrap() http.ResponseWriter` which eventually should return echo.Response instance.
// SetResponse sets `*http.ResponseWriter`. Some context methods and/or middleware require that given ResponseWriter implements following
// method `Unwrap() http.ResponseWriter` which eventually should return *echo.Response instance.
func (c *Context) SetResponse(r http.ResponseWriter) {
c.response = r
}
Expand Down Expand Up @@ -453,7 +453,16 @@ func (c *Context) jsonPBlob(code int, callback string, i any) (err error) {

func (c *Context) json(code int, i any, indent string) error {
c.writeContentType(MIMEApplicationJSON)
c.response.WriteHeader(code)

if r, err := UnwrapResponse(c.response); err == nil {
// *echo.Response can delay sending status code until the first Write is called. As serialization can fail, we should delay
// sending the status code to the client until serialization is complete (first Write would be an indication it succeeded)
// Unsuccessful serialization error needs to go through the error handler and get a proper status code there.
r.Status = code
} else {
return fmt.Errorf("json: response does not unwrap to *echo.Response")
}

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

Expand Down
42 changes: 38 additions & 4 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,24 @@ func TestContextRenderTemplate(t *testing.T) {
}
}

func TestContextRenderTemplateError(t *testing.T) {
// we test that when template rendering fails, no response is sent to the client yet, so the global error handler can decide what to do
e := New()
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
rec := httptest.NewRecorder()
c := e.NewContext(req, rec)

tmpl := &Template{
templates: template.Must(template.New("hello").Parse("Hello, {{.}}!")),
}
c.Echo().Renderer = tmpl
err := c.Render(http.StatusOK, "not_existing", "Jon Snow")

assert.EqualError(t, err, `template: no template "not_existing" associated with template "hello"`)
assert.Equal(t, http.StatusOK, rec.Code) // status code must not be sent to the client
assert.Empty(t, rec.Body.String()) // body must not be sent to the client
}

func TestContextRenderErrorsOnNoRenderer(t *testing.T) {
e := New()
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
Expand Down Expand Up @@ -173,10 +191,9 @@ func TestContextStream(t *testing.T) {
}

func TestContextHTML(t *testing.T) {
e := New()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
c := e.NewContext(req, rec)
c := NewContext(req, rec)

err := c.HTML(http.StatusOK, "Hi, Jon Snow")
if assert.NoError(t, err) {
Expand All @@ -187,10 +204,9 @@ func TestContextHTML(t *testing.T) {
}

func TestContextHTMLBlob(t *testing.T) {
e := New()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/", nil)
c := e.NewContext(req, rec)
c := NewContext(req, rec)

err := c.HTMLBlob(http.StatusOK, []byte("Hi, Jon Snow"))
if assert.NoError(t, err) {
Expand Down Expand Up @@ -222,6 +238,24 @@ func TestContextJSONErrorsOut(t *testing.T) {

err := c.JSON(http.StatusOK, make(chan bool))
assert.EqualError(t, err, "json: unsupported type: chan bool")

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

func TestContextJSONWithNotEchoResponse(t *testing.T) {
e := New()
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(userJSON))
c := e.NewContext(req, rec)

c.SetResponse(rec)

err := c.JSON(http.StatusOK, map[string]interface{}{"foo": "bar"})
assert.EqualError(t, err, "json: response does not unwrap to *echo.Response")

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

func TestContextJSONPretty(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion response.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ func UnwrapResponse(rw http.ResponseWriter) (*Response, error) {
rw = t.Unwrap()
continue
default:
return nil, errors.New("ResponseWriter does not implement 'Unwrap() http.ResponseWriter' interface")
return nil, errors.New("ResponseWriter does not implement 'Unwrap() http.ResponseWriter' interface or unwrap to *echo.Response")
}
}
}
16 changes: 16 additions & 0 deletions response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,19 @@ func TestResponse_FlushPanics(t *testing.T) {
res.Flush()
})
}

func TestResponse_UnwrapResponse(t *testing.T) {
orgRes := NewResponse(httptest.NewRecorder(), nil)
res, err := UnwrapResponse(orgRes)

assert.NotNil(t, res)
assert.NoError(t, err)
}

func TestResponse_UnwrapResponse_error(t *testing.T) {
rw := new(testResponseWriter)
res, err := UnwrapResponse(rw)

assert.Nil(t, res)
assert.EqualError(t, err, "ResponseWriter does not implement 'Unwrap() http.ResponseWriter' interface or unwrap to *echo.Response")
}
Loading