Skip to content

feat: add RFC 9457 Problem Details HTTP error handler - #3062

Merged
aldas merged 1 commit into
labstack:masterfrom
miqui:feat/rfc9457-problem-details
Jul 30, 2026
Merged

feat: add RFC 9457 Problem Details HTTP error handler#3062
aldas merged 1 commit into
labstack:masterfrom
miqui:feat/rfc9457-problem-details

Conversation

@miqui

@miqui miqui commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Follows up on Support RFC9457 by default #3053 (comment: Support RFC9457 by default #3053 (comment)).
  • Adds ProblemError, a struct with the RFC 9457 (Problem Details for HTTP APIs) fields: Type, Title, Status, Detail, Instance.
  • Adds ProblemErrorer, an interface custom error types can implement to control how they convert into a *ProblemError.
  • Adds ProblemDetailsHTTPErrorHandler(exposeError bool), a separate HTTPErrorHandler implementation (does not touch DefaultHTTPErrorHandler) that renders every error as application/problem+json, per @aldas's guidance in the discussion.
  • Adds MIMEApplicationProblemJSON constant alongside the existing MIME* constants.

Design notes

  • Precedence when building the response: *ProblemError (or one wrapped by err) is used as-is → else ProblemErrorer is used if implemented → else one is built from the error's HTTPStatusCoder status (default 500) and, for *HTTPError, its Message (further extended with the wrapped error's message when exposeError is true, mirroring DefaultHTTPErrorHandler's exposeError semantics). Zero-value Type/Title/Status default to "about:blank", the status text, and 500 respectively.

Test plan

  • go build ./...
  • go vet ./...
  • go test ./...
  • go test -race ./...
  • New unit tests in rfc9457_test.go covering *HTTPError (with/without message, with/without wrapped errors, exposeError true/false), plain errors, *ProblemError passed directly, ProblemErrorer implementations (including nil-return safety), HEAD requests, and the committed-response short-circuit.

🤖 Generated with Claude Code

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>
@miqui

miqui commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

@aldas

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:

  • No content negotiation — every error becomes problem+json, including for clients that asked for HTML or XML. IMHO, I think this ok.

  • A wrapped *ProblemError wins over the outer *HTTPError's status, since rule 1 matches anywhere in the chain. Intentional, but surprising if you wrap a problem error in an HTTPError with a different code.
    A wrapped *ProblemError wins over the outer *HTTPError's status, since rule 1 matches anywhere in the chain. Intentional, but surprising if you wrap a problem error in an HTTPError with a different code.

	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.
   	
   	
   	
   	
   	

@miqui

miqui commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

note: no AI slop. I verified everything and made every decision.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.83333% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.37%. Comparing base (dcb05f0) to head (a5e7ff5).
⚠️ Report is 6 commits behind head on master.

Files with missing lines Patch % Lines
rfc9457.go 95.83% 1 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aldas aldas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@aldas
aldas merged commit 39f61ef into labstack:master Jul 30, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants