feat: add RFC 9457 Problem Details HTTP error handler - #3062
Conversation
Adds ProblemError (RFC 9457 fields), a ProblemErrorer interface so custom error types can convert themselves to a ProblemError, and a standalone ProblemDetailsHTTPErrorHandler that renders errors as application/problem+json, as discussed in labstack#3053. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
usage: e := echo.New()
e.HTTPErrorHandler = echo.ProblemDetailsHTTPErrorHandler(false) // true in dev to leak internals
e.POST("/orders", func(c *echo.Context) error {
return &echo.ProblemError{
Type: "https://example.com/probs/out-of-credit",
Title: "You do not have enough credit.",
Status: http.StatusForbidden,
Detail: "Your current balance is 30, but that costs 50.",
Instance: "/account/12345/msgs/abc",
}
})Some points to consider:
switch {
case errors.As(err, &pe): // target: **ProblemError
case errors.As(err, &pder): // target: *ProblemErrorer
default:
...
var sc HTTPStatusCoder // target: *HTTPStatusCoder
if errors.As(err, &sc) {
pe.Status = sc.StatusCode()
}
```
Given HTTPError{Code: 500}.Wrap(&ProblemError{Status: 403}), the chain is *HTTPError → *ProblemError:
• Target HTTPStatusCoder (an interface): *HTTPError satisfies it, so it matches at link 1 → 500.
• Target *ProblemError (a concrete type): *HTTPError is not that type, so it's skipped; match at link 2 → 403.
Because the *ProblemError check is rule 1 in the switch, it runs before the HTTPStatusCoder lookup ever happens. The outer error's status is never even read.
|
|
note: no AI slop. I verified everything and made every decision. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3062 +/- ##
==========================================
+ Coverage 93.34% 93.37% +0.02%
==========================================
Files 43 44 +1
Lines 4735 4783 +48
==========================================
+ Hits 4420 4466 +46
- Misses 192 193 +1
- Partials 123 124 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Summary
ProblemError, a struct with the RFC 9457 (Problem Details for HTTP APIs) fields:Type,Title,Status,Detail,Instance.ProblemErrorer, an interface custom error types can implement to control how they convert into a*ProblemError.ProblemDetailsHTTPErrorHandler(exposeError bool), a separateHTTPErrorHandlerimplementation (does not touchDefaultHTTPErrorHandler) that renders every error asapplication/problem+json, per @aldas's guidance in the discussion.MIMEApplicationProblemJSONconstant alongside the existingMIME*constants.Design notes
*ProblemError(or one wrapped byerr) is used as-is → elseProblemErroreris used if implemented → else one is built from the error'sHTTPStatusCoderstatus (default 500) and, for*HTTPError, itsMessage(further extended with the wrapped error's message whenexposeErroris true, mirroringDefaultHTTPErrorHandler'sexposeErrorsemantics). Zero-valueType/Title/Statusdefault to"about:blank", the status text, and 500 respectively.Test plan
go build ./...go vet ./...go test ./...go test -race ./...rfc9457_test.gocovering*HTTPError(with/without message, with/without wrapped errors,exposeErrortrue/false), plain errors,*ProblemErrorpassed directly,ProblemErrorerimplementations (including nil-return safety), HEAD requests, and the committed-response short-circuit.🤖 Generated with Claude Code