diff --git a/CHANGELOG.md b/CHANGELOG.md index 6013765..0b7175b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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/), 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 ### Added diff --git a/go.mod b/go.mod index f35356a..b31572b 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25 require ( 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 ( diff --git a/go.sum b/go.sum index acdeb32..21ffa88 100644 --- a/go.sum +++ b/go.sum @@ -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/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.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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= diff --git a/httputil_test.go b/httputil_test.go index 6c980b5..33faf8d 100644 --- a/httputil_test.go +++ b/httputil_test.go @@ -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) { cases := []struct { code xerrors.Code diff --git a/response.go b/response.go index 789aedc..41a4be8 100644 --- a/response.go +++ b/response.go @@ -22,28 +22,31 @@ func NoContent(w http.ResponseWriter) { } // 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. func Error(w http.ResponseWriter, err error) { if err == nil { - JSON(w, http.StatusInternalServerError, errorBody("INTERNAL", "internal server error", nil)) + JSON(w, http.StatusInternalServerError, errorBody("INTERNAL", "", "internal server error", nil)) return } var xe *xerrors.Err if errors.As(err, &xe) { 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) 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{ "code": code, "message": message, } + if platformCode != "" { + m["platformCode"] = platformCode + } for k, v := range fields { m[k] = v }