feat: include platformCode in error responses (v0.10.0) #1

Merged
Rene Nochebuena merged 1 commits from feature/platform-code into main 2026-03-25 16:57:50 -06:00
5 changed files with 47 additions and 6 deletions
Showing only changes of commit 49175e0bd5 - Show all commits

View File

@@ -5,6 +5,20 @@ All notable changes to this module will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.10.0] - 2026-03-25
### Changed
- `Error(w http.ResponseWriter, err error)` — now includes `"platformCode"` in the JSON error body when the `*xerrors.Err` carries one; omitted otherwise
- Upgraded `code.nochebuena.dev/go/xerrors` dependency to `v0.10.0`
### Design Notes
- `platformCode` is the domain-layer error identifier introduced in `xerrors` v0.10.0. It travels through `Error` transparently — no transport-layer logic depends on it.
- Backwards-compatible: error responses without a platform code are identical to v0.9.0.
[0.10.0]: https://code.nochebuena.dev/go/httputil/compare/v0.9.0...v0.10.0
## [0.9.0] - 2026-03-18 ## [0.9.0] - 2026-03-18
### Added ### Added

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.25
require ( require (
code.nochebuena.dev/go/valid v0.9.0 code.nochebuena.dev/go/valid v0.9.0
code.nochebuena.dev/go/xerrors v0.9.0 code.nochebuena.dev/go/xerrors v0.10.0
) )
require ( require (

2
go.sum
View File

@@ -2,6 +2,8 @@ code.nochebuena.dev/go/valid v0.9.0 h1:o8/tICIoed2+uwBp+TxXa3FE6KmyirU266O4jEUgF
code.nochebuena.dev/go/valid v0.9.0/go.mod h1:SKpLcqpEsLMaEk7K3Y0kFF7Y3W5PHAQF6+U6wleFAhg= code.nochebuena.dev/go/valid v0.9.0/go.mod h1:SKpLcqpEsLMaEk7K3Y0kFF7Y3W5PHAQF6+U6wleFAhg=
code.nochebuena.dev/go/xerrors v0.9.0 h1:8wrDto7e44ZW1YPOnT6JrxYXTqnvNuKpAO1/5bcT4TE= code.nochebuena.dev/go/xerrors v0.9.0 h1:8wrDto7e44ZW1YPOnT6JrxYXTqnvNuKpAO1/5bcT4TE=
code.nochebuena.dev/go/xerrors v0.9.0/go.mod h1:mtXo7xscBreCB7w7smlBP5Onv8H1HVohCvF0I/VXbAY= code.nochebuena.dev/go/xerrors v0.9.0/go.mod h1:mtXo7xscBreCB7w7smlBP5Onv8H1HVohCvF0I/VXbAY=
code.nochebuena.dev/go/xerrors v0.10.0 h1:/4BCGZ7yZ384IC7t8SWGuAqEH/FmI9WtxiB5RKIoKWc=
code.nochebuena.dev/go/xerrors v0.10.0/go.mod h1:mtXo7xscBreCB7w7smlBP5Onv8H1HVohCvF0I/VXbAY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=

View File

@@ -240,6 +240,28 @@ func TestError_NilError(t *testing.T) {
} }
} }
func TestError_PlatformCode_IncludedWhenSet(t *testing.T) {
rec := httptest.NewRecorder()
Error(rec, xerrors.New(xerrors.ErrNotFound, "employee not found").
WithPlatformCode("EMPLOYEE_NOT_FOUND"))
if rec.Code != http.StatusNotFound {
t.Errorf("want 404, got %d", rec.Code)
}
m := decodeMap(t, rec)
if m["platformCode"] != "EMPLOYEE_NOT_FOUND" {
t.Errorf("platformCode: want EMPLOYEE_NOT_FOUND, got %v", m["platformCode"])
}
}
func TestError_PlatformCode_OmittedWhenNotSet(t *testing.T) {
rec := httptest.NewRecorder()
Error(rec, xerrors.New(xerrors.ErrInternal, "unexpected error"))
m := decodeMap(t, rec)
if _, ok := m["platformCode"]; ok {
t.Errorf("platformCode should be absent for errors without a platform code, got %v", m["platformCode"])
}
}
func TestErrorCodeToStatus_AllCodes(t *testing.T) { func TestErrorCodeToStatus_AllCodes(t *testing.T) {
cases := []struct { cases := []struct {
code xerrors.Code code xerrors.Code

View File

@@ -22,28 +22,31 @@ func NoContent(w http.ResponseWriter) {
} }
// Error maps err to the appropriate HTTP status code and writes a JSON error body. // Error maps err to the appropriate HTTP status code and writes a JSON error body.
// Understands *xerrors.Err — extracts Code and Message; fields are included if present. // Understands *xerrors.Err — extracts Code, PlatformCode, and Message.
// Falls back to 500 for unknown errors. // Falls back to 500 for unknown errors.
func Error(w http.ResponseWriter, err error) { func Error(w http.ResponseWriter, err error) {
if err == nil { if err == nil {
JSON(w, http.StatusInternalServerError, errorBody("INTERNAL", "internal server error", nil)) JSON(w, http.StatusInternalServerError, errorBody("INTERNAL", "", "internal server error", nil))
return return
} }
var xe *xerrors.Err var xe *xerrors.Err
if errors.As(err, &xe) { if errors.As(err, &xe) {
status := errorCodeToStatus(xe.Code()) status := errorCodeToStatus(xe.Code())
body := errorBody(string(xe.Code()), xe.Message(), xe.Fields()) body := errorBody(string(xe.Code()), xe.PlatformCode(), xe.Message(), xe.Fields())
JSON(w, status, body) JSON(w, status, body)
return return
} }
JSON(w, http.StatusInternalServerError, errorBody("INTERNAL", "internal server error", nil)) JSON(w, http.StatusInternalServerError, errorBody("INTERNAL", "", "internal server error", nil))
} }
func errorBody(code, message string, fields map[string]any) map[string]any { func errorBody(code, platformCode, message string, fields map[string]any) map[string]any {
m := map[string]any{ m := map[string]any{
"code": code, "code": code,
"message": message, "message": message,
} }
if platformCode != "" {
m["platformCode"] = platformCode
}
for k, v := range fields { for k, v := range fields {
m[k] = v m[k] = v
} }