Skip to content
Merged
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
20 changes: 12 additions & 8 deletions rfc9457.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,23 +104,27 @@ func ProblemDetailsHTTPErrorHandler(exposeError bool) HTTPErrorHandler {
}
}

if pe.Status == 0 {
pe.Status = http.StatusInternalServerError
// The problem can be a value the application reuses, for example a package
// level sentinel error, so the defaults are applied to a copy. Writing them
// back would race between requests and would permanently alter the error.
problem := *pe
if problem.Status == 0 {
problem.Status = http.StatusInternalServerError
}
if pe.Type == "" {
pe.Type = "about:blank"
if problem.Type == "" {
problem.Type = "about:blank"
}
if pe.Title == "" {
pe.Title = http.StatusText(pe.Status)
if problem.Title == "" {
problem.Title = http.StatusText(problem.Status)
}

c.Response().Header().Set(HeaderContentType, MIMEApplicationProblemJSON)

var cErr error
if c.Request().Method == http.MethodHead { // Issue #608
cErr = c.NoContent(pe.Status)
cErr = c.NoContent(problem.Status)
} else {
cErr = c.JSON(pe.Status, pe)
cErr = c.JSON(problem.Status, &problem)
}
if cErr != nil {
c.Logger().Error("echo RFC 9457 error handler failed to send error to client", "error", cErr) // truly rare case. ala client already disconnected
Expand Down
19 changes: 19 additions & 0 deletions rfc9457_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,22 @@ func TestProblemError_StatusCode(t *testing.T) {
pe := &ProblemError{Status: http.StatusTeapot}
assert.Equal(t, http.StatusTeapot, pe.StatusCode())
}

Comment thread
aldas marked this conversation as resolved.
func TestProblemDetailsHTTPErrorHandler_DoesNotMutateSharedProblem(t *testing.T) {
// A package level sentinel is the idiomatic way to express a reusable error,
// so serving one must not leave the value altered.
sentinel := &ProblemError{Status: http.StatusNotFound, Detail: "no such widget"}
original := *sentinel

e := New()
e.Logger = slog.New(slog.DiscardHandler)
e.Any("/path", func(c *Context) error { return sentinel })
e.HTTPErrorHandler = ProblemDetailsHTTPErrorHandler(false)

rec := httptest.NewRecorder()
e.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/path", nil))

assert.Equal(t, http.StatusNotFound, rec.Code)
assert.Equal(t, `{"type":"about:blank","title":"Not Found","status":404,"detail":"no such widget"}`+"\n", rec.Body.String())
assert.Equal(t, original, *sentinel)
}
Loading