├── .github ├── release-drafter.yml └── workflows │ ├── ci.yml │ └── release-drafter-labeler.yml ├── .gitignore ├── CONTRIBUTION.md ├── LICENSE ├── README.md ├── assets ├── logo.png └── problem-details.png ├── go.mod ├── go.sum ├── problem_details.go ├── problem_details_test.go └── samples ├── cmd ├── echo │ └── main.go ├── fiber │ └── main.go └── gin │ └── main.go ├── custom-errors └── custom-error.go └── custom-problems └── custom-problem.go /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # https://johanneskonings.dev/github/2021/02/28/github_automatic_releases_and-changelog/ 2 | # https://tiagomichaelsousa.dev/articles/stop-writing-your-changelogs-manually 3 | # https://github.com/release-drafter/release-drafter/issues/551 4 | # https://github.com/release-drafter/release-drafter/pull/1013 5 | # https://github.com/release-drafter/release-drafter/issues/139 6 | # https://github.com/atk4/data/blob/develop/.github/release-drafter.yml 7 | 8 | # This release drafter follows the conventions from https://keepachangelog.com, https://common-changelog.org/ 9 | # https://www.conventionalcommits.org 10 | 11 | name-template: 'v$RESOLVED_VERSION' 12 | tag-template: 'v$RESOLVED_VERSION' 13 | template: | 14 | ## What Changed 👀 15 | $CHANGES 16 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION 17 | categories: 18 | - title: 🚀 Features 19 | labels: 20 | - feature 21 | - title: 🐛 Bug Fixes 22 | labels: 23 | - fix 24 | - bug 25 | - title: 🧪 Test 26 | labels: 27 | - test 28 | - title: 👷 CI 29 | labels: 30 | - ci 31 | - title: ♻️ Refactor 32 | labels: 33 | - changed 34 | - enhancement 35 | - refactor 36 | - title: ⛔️ Deprecated 37 | labels: 38 | - deprecated 39 | - title: 🔐 Security 40 | labels: 41 | - security 42 | - title: 📄 Documentation 43 | labels: 44 | - docs 45 | - documentation 46 | - title: 🧩 Dependency Updates 47 | labels: 48 | - deps 49 | - dependencies 50 | - title: 🧰 Maintenance 51 | label: 'chore' 52 | - title: 📝 Other changes 53 | ## putting no labels pr to `Other Changes` category with no label - https://github.com/release-drafter/release-drafter/issues/139#issuecomment-480473934 54 | 55 | # https://www.trywilco.com/post/wilco-ci-cd-github-heroku 56 | # https://github.com/release-drafter/release-drafter#autolabeler 57 | # https://github.com/fuxingloh/multi-labeler 58 | 59 | # Using regex for defining rules - https://regexr.com/ - https://regex101.com/ 60 | autolabeler: 61 | - label: 'chore' 62 | branch: 63 | - '/(chore)\/.*/' 64 | - label: 'security' 65 | branch: 66 | - '/(security)\/.*/' 67 | - label: 'refactor' 68 | branch: 69 | - '/(refactor)\/.*/' 70 | - label: 'docs' 71 | branch: 72 | - '/(docs)\/.*/' 73 | - label: 'ci' 74 | branch: 75 | - '/(ci)\/.*/' 76 | - label: 'test' 77 | branch: 78 | - '/(test)\/.*/' 79 | - label: 'bug' 80 | branch: 81 | - '/(fix)\/.*/' 82 | - label: 'feature' 83 | branch: 84 | - '/(feat)\/.*/' 85 | - label: 'minor' 86 | branch: 87 | - '/(feat)\/.*/' 88 | - label: 'patch' 89 | branch: 90 | - '/(fix)\/.*/' 91 | body: 92 | - '/JIRA-[0-9]{1,4}/' 93 | 94 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 95 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. 96 | version-resolver: 97 | major: 98 | labels: 99 | - major 100 | minor: 101 | labels: 102 | - minor 103 | patch: 104 | labels: 105 | - patch 106 | default: patch 107 | 108 | exclude-labels: 109 | - skip-changelog 110 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v3 18 | with: 19 | go-version: 1.23 20 | 21 | - name: build 22 | run: go build -v ./... 23 | 24 | - name: test 25 | run: go test -v -coverprofile=profile.cov ./... 26 | 27 | - name: send coverage 28 | uses: shogo82148/actions-goveralls@v1 29 | with: 30 | path-to-profile: profile.cov 31 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter-labeler.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter Auto Labeler 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | - reopened 9 | - labeled 10 | - unlabeled 11 | 12 | jobs: 13 | auto-labeler: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: release-drafter/release-drafter@v5 17 | with: 18 | config-name: release-drafter.yml 19 | disable-releaser: true # only run auto-labeler for PRs 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # Ignore idea 18 | .idea/** -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | ## Contribution 2 | 3 | This is great that you'd like to contribute to this project. All change requests should go through the steps described below. 4 | 5 | ## Pull Requests 6 | 7 | **Please, make sure you open an issue before starting with a Pull Request, unless it's a typo or a really obvious error.** Pull requests are the best way to propose changes. 8 | 9 | ## Conventional commits 10 | 11 | Our repository follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/#summary) specification. Releasing to GitHub and NuGet is done with the support of [semantic-release](https://semantic-release.gitbook.io/semantic-release/). 12 | 13 | Pull requests should have a title that follows the specification, otherwise, merging is blocked. If you are not familiar with the specification simply ask maintainers to modify. You can also use this cheatsheet if you want: 14 | 15 | - `fix: ` prefix in the title indicates that PR is a bug fix and PATCH release must be triggered. 16 | - `feat: ` prefix in the title indicates that PR is a feature and MINOR release must be triggered. 17 | - `docs: ` prefix in the title indicates that PR is only related to the documentation and there is no need to trigger release. 18 | - `chore: ` prefix in the title indicates that PR is only related to cleanup in the project and there is no need to trigger release. 19 | - `test: ` prefix in the title indicates that PR is only related to tests and there is no need to trigger release. 20 | - `refactor: ` prefix in the title indicates that PR is only related to refactoring and there is no need to trigger release. 21 | 22 | ## Resources 23 | 24 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 25 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 26 | - [GitHub Help](https://help.github.com) 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Meysam Hadeli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
13 | 14 | > ProblemDetails is a Error Handler base on [RFC 7807](https://datatracker.ietf.org/doc/html/rfc7807) standard to map our error to standardized error payload to client. The data model for problem details is a JSON object; when formatted as a JSON document, it uses the `application/problem+json` media type and for XML format it uses the `application/problem+xml` media type. By defining machine-readable details of HTTP errors, we can avoid defining new error response formats for HTTP APIs. 15 | 16 | Our problem details response body and headers will be look like this: 17 | ```go 18 | // Response body 19 | 20 | { 21 | "status": 400, // The HTTP status code generated on the problem occurrence 22 | "title": "bad-request", // A short human-readable problem summary 23 | "detail": "We have a bad request in our endpoint", // A human-readable explanation for what exactly happened 24 | "type": "https://httpstatuses.io/400", // URI reference to identify the problem type 25 | "instance": "/sample1", // URI reference of the occurrence 26 | "stackTrace": "some more trace for error", // More trace information error for what exactly happened 27 | } 28 | ``` 29 | ```go 30 | // Response headers 31 | 32 | content-type: application/problem+json 33 | date: Thu,29 Sep 2022 14:07:23 GMT 34 | ``` 35 | There are some samples for using this package on top of Echo [here](./sample/cmd/echo/main.go), Fiber [here](./sample/cmd/fiber/main.go) and for Gin [here](./sample/cmd/gin/main.go). 36 | 37 | ## Installation 38 | 39 | ```bash 40 | go get github.com/meysamhadeli/problem-details 41 | ``` 42 | 43 | ## Web-Frameworks 44 | 45 | > ### Echo 46 | 47 | #### Error Handler: 48 | For handling our error we need to specify an `Error Handler` on top of `Echo` framework: 49 | ```go 50 | // EchoErrorHandler middleware for handle problem details error on echo 51 | func EchoErrorHandler(error error, c echo.Context) { 52 | 53 | // add custom map problem details here... 54 | 55 | // resolve problem details error from response in echo 56 | if !c.Response().Committed { 57 | if _, err := problem.ResolveProblemDetails(c.Response(), c.Request(), error); err != nil { 58 | log.Error(err) 59 | } 60 | } 61 | } 62 | ``` 63 | 64 | #### Map Status Code Error: 65 | 66 | In this sample we map status code `StatusBadGateway` to `StatusUnauthorized` base on handler config to problem details error. 67 | 68 | ```go 69 | // handle specific status code to problem details error 70 | func sample1(c echo.Context) error { 71 | err := errors.New("We have a specific status code error in our endpoint") 72 | return echo.NewHTTPError(http.StatusBadGateway, err) 73 | } 74 | ``` 75 | ```go 76 | // problem details handler config 77 | problem.MapStatus(http.StatusBadGateway, func() problem.ProblemDetailErr { 78 | return &problem.ProblemDetail{ 79 | Status: http.StatusUnauthorized, 80 | Title: "unauthorized", 81 | Detail: error.Error(), 82 | } 83 | }) 84 | ``` 85 | #### Map Custom Type Error: 86 | 87 | In this sample we map custom error type to problem details error. 88 | 89 | ```go 90 | // handle custom type error to problem details error 91 | func sample2(c echo.Context) error { 92 | err := errors.New("We have a custom type error in our endpoint") 93 | return custom_errors.BadRequestError{InternalError: err} 94 | } 95 | ``` 96 | ```go 97 | // problem details handler config 98 | problem.Map[custom_errors.BadRequestError](func() problem.ProblemDetailErr { 99 | return &problem.ProblemDetail{ 100 | Status: http.StatusBadRequest, 101 | Title: "bad request", 102 | Detail: error.Error(), 103 | } 104 | }) 105 | ``` 106 | 107 | > ### Fiber 108 | #### Error Handler: 109 | For handling our error we need to specify an `Error Handler` on top of `Fiber` framework: 110 | ```go 111 | // FiberErrorHandler middleware for handle problem details error on fiber 112 | func FiberErrorHandler(c fiber.Ctx) error { 113 | err := c.Next() 114 | 115 | if err != nil { 116 | 117 | // add custom map problem details here... 118 | 119 | // resolve problem details error from response in fiber 120 | if _, err := problem.ResolveProblemDetails(problem.Response(c), problem.Request(c), err); err != nil { 121 | log.Error(err) 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | ``` 128 | 129 | #### Map Status Code Error: 130 | 131 | In this sample we map status code `StatusBadGateway` to `StatusUnauthorized` base on handler config to problem details error. 132 | 133 | ```go 134 | // handle specific status code to problem details error 135 | func sample1(c fiber.Ctx) error { 136 | err := errors.New("We have a specific status code error in our endpoint") 137 | return fiber.NewError(http.StatusBadGateway, err.Error()) 138 | } 139 | ``` 140 | ```go 141 | // problem details handler config 142 | problem.MapStatus(http.StatusBadGateway, func() problem.ProblemDetailErr { 143 | return &problem.ProblemDetail{ 144 | Status: http.StatusUnauthorized, 145 | Title: "unauthorized", 146 | Detail: error.Error(), 147 | } 148 | }) 149 | ``` 150 | #### Map Custom Type Error: 151 | 152 | In this sample we map custom error type to problem details error. 153 | 154 | ```go 155 | // handle custom type error to problem details error 156 | func sample2(c fiber.Ctx) error { 157 | err := errors.New("We have a custom type error in our endpoint") 158 | return custom_errors.BadRequestError{InternalError: err} 159 | } 160 | ``` 161 | ```go 162 | // problem details handler config 163 | problem.Map[custom_errors.BadRequestError](func() problem.ProblemDetailErr { 164 | return &problem.ProblemDetail{ 165 | Status: http.StatusBadRequest, 166 | Title: "bad request", 167 | Detail: error.Error(), 168 | } 169 | }) 170 | ``` 171 | 172 | #### Custom Problem Details: 173 | 174 | We support custom problem details error for create more flexibility response error: 175 | ```go 176 | // custom problem details 177 | type CustomProblemDetail struct { 178 | problem.ProblemDetailErr 179 | Description string `json:"description,omitempty"` 180 | AdditionalInfo string `json:"additionalInfo,omitempty"` 181 | } 182 | ``` 183 | ```go 184 | // problem details handler config 185 | problem.Map[custom_errors.ConflictError](func() problem.ProblemDetailErr { 186 | return &custom_problems.CustomProblemDetail{ 187 | ProblemDetailErr: &problem.ProblemDetail{ 188 | Status: http.StatusConflict, 189 | Title: "conflict", 190 | Detail: error.Error(), 191 | }, 192 | AdditionalInfo: "some additional info...", 193 | Description: "some description...", 194 | } 195 | }) 196 | ``` 197 | 198 | 199 | > ### Gin 200 | #### Error Handler: 201 | For handling our error we need to specify an `Error Handler` on top of `Gin` framework: 202 | ```go 203 | // GinErrorHandler middleware for handle problem details error on gin 204 | func GinErrorHandler() gin.HandlerFunc { 205 | return func(c *gin.Context) { 206 | 207 | c.Next() 208 | 209 | for _, err := range c.Errors { 210 | 211 | // add custom map problem details here... 212 | 213 | if _, err := problem.ResolveProblemDetails(c.Writer, c.Request, err); err != nil { 214 | log.Error(err) 215 | } 216 | } 217 | } 218 | } 219 | ``` 220 | 221 | #### Map Status Code Error: 222 | 223 | In this sample we map status code `StatusBadGateway` to `StatusUnauthorized` base on handler config to problem details error. 224 | 225 | ```go 226 | // handle specific status code to problem details error 227 | func sample1(c *gin.Context) { 228 | err := errors.New("We have a specific status code error in our endpoint") 229 | _ = c.AbortWithError(http.StatusBadGateway, err) 230 | } 231 | ``` 232 | ```go 233 | // problem details handler config 234 | problem.MapStatus(http.StatusBadGateway, func() problem.ProblemDetailErr { 235 | return &problem.ProblemDetail{ 236 | Status: http.StatusUnauthorized, 237 | Title: "unauthorized", 238 | Detail: err.Error(), 239 | } 240 | }) 241 | ``` 242 | #### Map Custom Type Error: 243 | 244 | In this sample we map custom error type to problem details error. 245 | 246 | ```go 247 | // handle custom type error to problem details error 248 | func sample2(c *gin.Context) { 249 | err := errors.New("We have a custom type error in our endpoint") 250 | customBadRequestError := custom_errors.BadRequestError{InternalError: err} 251 | _ = c.Error(customBadRequestError) 252 | } 253 | ``` 254 | ```go 255 | // problem details handler config 256 | problem.Map[custom_errors.BadRequestError](func() problem.ProblemDetailErr { 257 | return &problem.ProblemDetail{ 258 | Status: http.StatusBadRequest, 259 | Title: "bad request", 260 | Detail: err.Error(), 261 | } 262 | }) 263 | ``` 264 | 265 | #### Custom Problem Details: 266 | 267 | We support custom problem details error for create more flexibility response error: 268 | ```go 269 | // custom problem details 270 | type CustomProblemDetail struct { 271 | problem.ProblemDetailErr 272 | Description string `json:"description,omitempty"` 273 | AdditionalInfo string `json:"additionalInfo,omitempty"` 274 | } 275 | ``` 276 | ```go 277 | // problem details handler config 278 | problem.Map[custom_errors.ConflictError](func() problem.ProblemDetailErr { 279 | return &custom_problems.CustomProblemDetail{ 280 | ProblemDetailErr: &problem.ProblemDetail{ 281 | Status: http.StatusConflict, 282 | Title: "conflict", 283 | Detail: error.Error(), 284 | }, 285 | AdditionalInfo: "some additional info...", 286 | Description: "some description...", 287 | } 288 | }) 289 | ``` 290 | 291 | # Support 292 | 293 | If you like my work, feel free to: 294 | 295 | - ⭐ this repository. And we will be happy together :) 296 | 297 | Thanks a bunch for supporting me! 298 | 299 | ## Contribution 300 | 301 | Thanks to all [contributors](https://github.com/meysamhadeli/problem-details/graphs/contributors), you're awesome and this wouldn't be possible without you! The goal is to build a categorized community-driven collection of very well-known resources. 302 | 303 | Please follow this [contribution guideline](./CONTRIBUTION.md) to submit a pull request or create the issue. 304 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meysamhadeli/problem-details/e1d63703a71848a5ba2bfaa4de4e0ba4b97a6257/assets/logo.png -------------------------------------------------------------------------------- /assets/problem-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meysamhadeli/problem-details/e1d63703a71848a5ba2bfaa4de4e0ba4b97a6257/assets/problem-details.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/meysamhadeli/problem-details 2 | 3 | go 1.23.2 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.10.0 7 | github.com/gofiber/fiber/v3 v3.0.0-beta.4 8 | github.com/labstack/echo/v4 v4.12.0 9 | github.com/labstack/gommon v0.4.2 10 | github.com/pkg/errors v0.9.1 11 | github.com/stretchr/testify v1.10.0 12 | github.com/valyala/fasthttp v1.62.0 13 | ) 14 | 15 | require ( 16 | github.com/andybalholm/brotli v1.1.1 // indirect 17 | github.com/bytedance/sonic v1.12.3 // indirect 18 | github.com/bytedance/sonic/loader v0.2.1 // indirect 19 | github.com/cloudwego/base64x v0.1.4 // indirect 20 | github.com/cloudwego/iasm v0.2.0 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 23 | github.com/gabriel-vasile/mimetype v1.4.6 // indirect 24 | github.com/gin-contrib/sse v0.1.0 // indirect 25 | github.com/go-playground/locales v0.14.1 // indirect 26 | github.com/go-playground/universal-translator v0.18.1 // indirect 27 | github.com/go-playground/validator/v10 v10.22.1 // indirect 28 | github.com/goccy/go-json v0.10.3 // indirect 29 | github.com/gofiber/schema v1.3.0 // indirect 30 | github.com/gofiber/utils/v2 v2.0.0-beta.8 // indirect 31 | github.com/google/uuid v1.6.0 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/klauspost/compress v1.18.0 // indirect 34 | github.com/klauspost/cpuid/v2 v2.2.8 // indirect 35 | github.com/leodido/go-urn v1.4.0 // indirect 36 | github.com/mattn/go-colorable v0.1.14 // indirect 37 | github.com/mattn/go-isatty v0.0.20 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 41 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect 42 | github.com/pmezard/go-difflib v1.0.0 // indirect 43 | github.com/tinylib/msgp v1.2.5 // indirect 44 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 45 | github.com/ugorji/go/codec v1.2.12 // indirect 46 | github.com/valyala/bytebufferpool v1.0.0 // indirect 47 | github.com/valyala/fasttemplate v1.2.2 // indirect 48 | github.com/x448/float16 v0.8.4 // indirect 49 | golang.org/x/arch v0.11.0 // indirect 50 | golang.org/x/crypto v0.38.0 // indirect 51 | golang.org/x/net v0.40.0 // indirect 52 | golang.org/x/sys v0.33.0 // indirect 53 | golang.org/x/text v0.25.0 // indirect 54 | google.golang.org/protobuf v1.35.1 // indirect 55 | gopkg.in/yaml.v3 v3.0.1 // indirect 56 | ) 57 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= 2 | github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA= 3 | github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= 4 | github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= 5 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 6 | github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E= 7 | github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 8 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 9 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 10 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 11 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 16 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 17 | github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= 18 | github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= 19 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 20 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 21 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 22 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 23 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 24 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 25 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 26 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 27 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 28 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 29 | github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= 30 | github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 31 | github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= 32 | github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 33 | github.com/gofiber/fiber/v3 v3.0.0-beta.4 h1:KzDSavvhG7m81NIsmnu5l3ZDbVS4feCidl4xlIfu6V0= 34 | github.com/gofiber/fiber/v3 v3.0.0-beta.4/go.mod h1:/WFUoHRkZEsGHyy2+fYcdqi109IVOFbVwxv1n1RU+kk= 35 | github.com/gofiber/schema v1.3.0 h1:K3F3wYzAY+aivfCCEHPufCthu5/13r/lzp1nuk6mr3Q= 36 | github.com/gofiber/schema v1.3.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c= 37 | github.com/gofiber/utils/v2 v2.0.0-beta.8 h1:ZifwbHZqZO3YJsx1ZhDsWnPjaQ7C0YD20LHt+DQeXOU= 38 | github.com/gofiber/utils/v2 v2.0.0-beta.8/go.mod h1:1lCBo9vEF4RFEtTgWntipnaScJZQiM8rrsYycLZ4n9c= 39 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 40 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 42 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 43 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 44 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 45 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 46 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 47 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 48 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 49 | github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= 50 | github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 51 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 52 | github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= 53 | github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= 54 | github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= 55 | github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= 56 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 57 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 58 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 59 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 60 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 61 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 62 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 63 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 64 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 65 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 66 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 67 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 68 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 69 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY= 70 | github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM= 71 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 72 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 73 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 74 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 77 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 78 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 79 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 80 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 81 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 82 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 83 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 84 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 85 | github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po= 86 | github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0= 87 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 88 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 89 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 90 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 91 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= 92 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 93 | github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0= 94 | github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg= 95 | github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= 96 | github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= 97 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 98 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 99 | github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= 100 | github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= 101 | golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= 102 | golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 103 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 104 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 105 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 106 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 107 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 108 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 110 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 111 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 112 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 113 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 114 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 115 | google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= 116 | google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 117 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 118 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 119 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 120 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 121 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 122 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 123 | -------------------------------------------------------------------------------- /problem_details.go: -------------------------------------------------------------------------------- 1 | package problem 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gin-gonic/gin" 7 | "github.com/gofiber/fiber/v3" 8 | "github.com/labstack/echo/v4" 9 | "github.com/pkg/errors" 10 | "net/http" 11 | "net/http/httptest" 12 | "net/url" 13 | "reflect" 14 | ) 15 | 16 | type ProblemDetail struct { 17 | Status int `json:"status,omitempty"` 18 | Title string `json:"title,omitempty"` 19 | Detail string `json:"detail,omitempty"` 20 | Type string `json:"type,omitempty"` 21 | Instance string `json:"instance,omitempty"` 22 | StackTrace string `json:"stackTrace,omitempty"` 23 | } 24 | 25 | type fiberResponseWriter struct { 26 | ctx fiber.Ctx 27 | written bool 28 | headers http.Header 29 | } 30 | 31 | var mappers = map[reflect.Type]func() ProblemDetailErr{} 32 | var mapperStatus = map[int]func() ProblemDetailErr{} 33 | 34 | // ProblemDetailErr ProblemDetail error interface 35 | type ProblemDetailErr interface { 36 | SetStatus(status int) ProblemDetailErr 37 | GetStatus() int 38 | SetTitle(title string) ProblemDetailErr 39 | GetTitle() string 40 | SetDetail(detail string) ProblemDetailErr 41 | GetDetails() string 42 | SetType(typ string) ProblemDetailErr 43 | GetType() string 44 | SetInstance(instance string) ProblemDetailErr 45 | GetInstance() string 46 | SetStackTrace(stackTrace string) ProblemDetailErr 47 | GetStackTrace() string 48 | } 49 | 50 | func (p *ProblemDetail) SetDetail(detail string) ProblemDetailErr { 51 | p.Detail = detail 52 | 53 | return p 54 | } 55 | 56 | func (p *ProblemDetail) GetDetails() string { 57 | return p.Detail 58 | } 59 | 60 | func (p *ProblemDetail) SetStatus(status int) ProblemDetailErr { 61 | p.Status = status 62 | 63 | return p 64 | } 65 | 66 | func (p *ProblemDetail) GetStatus() int { 67 | return p.Status 68 | } 69 | 70 | func (p *ProblemDetail) SetTitle(title string) ProblemDetailErr { 71 | p.Title = title 72 | 73 | return p 74 | } 75 | 76 | func (p *ProblemDetail) GetTitle() string { 77 | return p.Title 78 | } 79 | 80 | func (p *ProblemDetail) SetType(typ string) ProblemDetailErr { 81 | p.Type = typ 82 | 83 | return p 84 | } 85 | 86 | func (p *ProblemDetail) GetType() string { 87 | return p.Type 88 | } 89 | 90 | func (p *ProblemDetail) SetInstance(instance string) ProblemDetailErr { 91 | p.Instance = instance 92 | 93 | return p 94 | } 95 | 96 | func (p *ProblemDetail) GetInstance() string { 97 | return p.Instance 98 | } 99 | 100 | func (p *ProblemDetail) SetStackTrace(stackTrace string) ProblemDetailErr { 101 | p.StackTrace = stackTrace 102 | 103 | return p 104 | } 105 | 106 | func (p *ProblemDetail) GetStackTrace() string { 107 | return p.StackTrace 108 | } 109 | 110 | func writeTo(w http.ResponseWriter, p ProblemDetailErr) (int, error) { 111 | 112 | w.Header().Set("Content-Type", "application/problem+json") 113 | w.WriteHeader(p.GetStatus()) 114 | 115 | val, err := json.Marshal(p) 116 | if err != nil { 117 | return 0, err 118 | } 119 | return w.Write(val) 120 | } 121 | 122 | // MapStatus map status code to problem details error 123 | func MapStatus(statusCode int, funcProblem func() ProblemDetailErr) { 124 | mapperStatus[statusCode] = funcProblem 125 | } 126 | 127 | // Map map custom type error to problem details error 128 | func Map[T error](funcProblem func() ProblemDetailErr) { 129 | mappers[reflect.TypeOf(*new(T))] = funcProblem 130 | } 131 | 132 | // ResolveProblemDetails retrieve and resolve error with format problem details error 133 | func ResolveProblemDetails(w http.ResponseWriter, r *http.Request, err error) (ProblemDetailErr, error) { 134 | 135 | var statusCode int = http.StatusInternalServerError 136 | var echoError *echo.HTTPError 137 | var ginError *gin.Error 138 | var fiberError *fiber.Error 139 | 140 | if errors.As(err, &fiberError) { 141 | statusCode = fiberError.Code 142 | err = errors.New(fiberError.Message) 143 | } else if errors.As(err, &echoError) { 144 | statusCode = err.(*echo.HTTPError).Code 145 | err = err.(*echo.HTTPError).Message.(error) 146 | } else if errors.As(err, &ginError) { 147 | var rw, ok = w.(gin.ResponseWriter) 148 | if ok && rw.Written() { 149 | statusCode = rw.Status() 150 | } 151 | if gin.Mode() == gin.TestMode { 152 | var rw = w.(*httptest.ResponseRecorder) 153 | if rw.Code != http.StatusOK { 154 | statusCode = rw.Code 155 | } 156 | } 157 | err = err.(*gin.Error).Err.(error) 158 | } 159 | 160 | var mapCustomType, mapCustomTypeErr = setMapCustomType(w, r, err) 161 | if mapCustomType != nil { 162 | return mapCustomType, mapCustomTypeErr 163 | } 164 | 165 | var mapStatus, mapStatusErr = setMapStatusCode(w, r, err, statusCode) 166 | if mapStatus != nil { 167 | return mapStatus, mapStatusErr 168 | } 169 | 170 | var p, errr = setDefaultProblemDetails(w, r, err, statusCode) 171 | if errr != nil { 172 | return nil, err 173 | } 174 | return p, errr 175 | } 176 | 177 | func setMapCustomType(w http.ResponseWriter, r *http.Request, err error) (ProblemDetailErr, error) { 178 | 179 | problemCustomType := mappers[reflect.TypeOf(err)] 180 | if problemCustomType != nil { 181 | prob := problemCustomType() 182 | 183 | validationProblems(prob, err, r) 184 | 185 | for k, v := range mapperStatus { 186 | if k == prob.GetStatus() { 187 | _, err = writeTo(w, v()) 188 | if err != nil { 189 | return nil, err 190 | } 191 | return prob, err 192 | } 193 | } 194 | 195 | _, err = writeTo(w, prob) 196 | if err != nil { 197 | return nil, err 198 | } 199 | return prob, err 200 | } 201 | return nil, err 202 | } 203 | 204 | func setMapStatusCode(w http.ResponseWriter, r *http.Request, err error, statusCode int) (ProblemDetailErr, error) { 205 | problemStatus := mapperStatus[statusCode] 206 | if problemStatus != nil { 207 | prob := problemStatus() 208 | validationProblems(prob, err, r) 209 | _, err = writeTo(w, prob) 210 | if err != nil { 211 | return nil, err 212 | } 213 | return prob, err 214 | } 215 | return nil, err 216 | } 217 | 218 | func setDefaultProblemDetails(w http.ResponseWriter, r *http.Request, err error, statusCode int) (ProblemDetailErr, error) { 219 | defaultProblem := func() ProblemDetailErr { 220 | return &ProblemDetail{ 221 | Type: getDefaultType(statusCode), 222 | Status: statusCode, 223 | Detail: err.Error(), 224 | Title: http.StatusText(statusCode), 225 | Instance: r.URL.RequestURI(), 226 | StackTrace: errorsWithStack(err), 227 | } 228 | } 229 | prob := defaultProblem() 230 | _, err = writeTo(w, prob) 231 | if err != nil { 232 | return nil, err 233 | } 234 | return prob, err 235 | } 236 | 237 | func validationProblems(problem ProblemDetailErr, err error, r *http.Request) { 238 | problem.SetDetail(err.Error()) 239 | 240 | if problem.GetStatus() == 0 { 241 | problem.SetStatus(http.StatusInternalServerError) 242 | } 243 | if problem.GetInstance() == "" { 244 | problem.SetInstance(r.URL.RequestURI()) 245 | } 246 | if problem.GetType() == "" { 247 | problem.SetType(getDefaultType(problem.GetStatus())) 248 | } 249 | if problem.GetTitle() == "" { 250 | problem.SetTitle(http.StatusText(problem.GetStatus())) 251 | } 252 | if problem.GetStackTrace() == "" { 253 | problem.SetStackTrace(errorsWithStack(err)) 254 | } 255 | } 256 | 257 | func getDefaultType(statusCode int) string { 258 | return fmt.Sprintf("https://httpstatuses.io/%d", statusCode) 259 | } 260 | 261 | func errorsWithStack(err error) string { 262 | res := fmt.Sprintf("%+v", err) 263 | return res 264 | } 265 | 266 | func Response(c fiber.Ctx) *fiberResponseWriter { 267 | return &fiberResponseWriter{ 268 | ctx: c, 269 | headers: make(http.Header), 270 | } 271 | } 272 | 273 | func (f *fiberResponseWriter) Header() http.Header { 274 | return f.headers 275 | } 276 | 277 | func (f *fiberResponseWriter) Write(data []byte) (int, error) { 278 | f.written = true 279 | return f.ctx.Response().BodyWriter().Write(data) 280 | } 281 | 282 | func (f *fiberResponseWriter) WriteHeader(statusCode int) { 283 | f.written = true 284 | f.ctx.Status(statusCode) 285 | } 286 | 287 | func Request(c fiber.Ctx) *http.Request { 288 | fiberURI := c.Request().URI() 289 | parsedURL, _ := url.Parse(string(fiberURI.FullURI())) 290 | 291 | return &http.Request{ 292 | Method: c.Method(), 293 | URL: parsedURL, 294 | Header: make(http.Header), 295 | RequestURI: string(c.Request().RequestURI()), 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /problem_details_test.go: -------------------------------------------------------------------------------- 1 | package problem 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "github.com/gofiber/fiber/v3" 7 | "github.com/labstack/echo/v4" 8 | custom_errors "github.com/meysamhadeli/problem-details/samples/custom-errors" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/valyala/fasthttp" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | ) 15 | 16 | func TestMap_CustomType_Echo(t *testing.T) { 17 | 18 | e := echo.New() 19 | req := httptest.NewRequest(http.MethodGet, "http://echo_endpoint1", nil) 20 | rec := httptest.NewRecorder() 21 | c := e.NewContext(req, rec) 22 | 23 | err := echo_endpoint1(c) 24 | 25 | Map[custom_errors.BadRequestError](func() ProblemDetailErr { 26 | return &ProblemDetail{ 27 | Status: http.StatusBadRequest, 28 | Title: "bad-request", 29 | Detail: err.Error(), 30 | } 31 | }) 32 | 33 | p, _ := ResolveProblemDetails(c.Response(), c.Request(), err) 34 | 35 | assert.Equal(t, c.Response().Status, http.StatusBadRequest) 36 | assert.Equal(t, err.Error(), p.GetDetails()) 37 | assert.Equal(t, "bad-request", p.GetTitle()) 38 | assert.Equal(t, "https://httpstatuses.io/400", p.GetType()) 39 | assert.Equal(t, http.StatusBadRequest, p.GetStatus()) 40 | } 41 | 42 | func TestMap_Custom_Problem_Err_Echo(t *testing.T) { 43 | 44 | e := echo.New() 45 | req := httptest.NewRequest(http.MethodGet, "http://echo_endpoint4", nil) 46 | rec := httptest.NewRecorder() 47 | c := e.NewContext(req, rec) 48 | 49 | err := echo_endpoint4(c) 50 | 51 | Map[custom_errors.ConflictError](func() ProblemDetailErr { 52 | return &CustomProblemDetailTest{ 53 | ProblemDetailErr: &ProblemDetail{ 54 | Status: http.StatusConflict, 55 | Title: "conflict", 56 | Detail: err.Error(), 57 | }, 58 | AdditionalInfo: "some additional info...", 59 | Description: "some description...", 60 | } 61 | }) 62 | 63 | p, _ := ResolveProblemDetails(c.Response(), c.Request(), err) 64 | cp := p.(*CustomProblemDetailTest) 65 | 66 | assert.Equal(t, c.Response().Status, http.StatusConflict) 67 | assert.Equal(t, err.Error(), cp.GetDetails()) 68 | assert.Equal(t, "conflict", cp.GetTitle()) 69 | assert.Equal(t, "https://httpstatuses.io/409", cp.GetType()) 70 | assert.Equal(t, http.StatusConflict, cp.GetStatus()) 71 | assert.Equal(t, "some description...", cp.Description) 72 | assert.Equal(t, "some additional info...", cp.AdditionalInfo) 73 | } 74 | 75 | func TestMap_Status_Echo(t *testing.T) { 76 | 77 | e := echo.New() 78 | req := httptest.NewRequest(http.MethodGet, "http://echo_endpoint2", nil) 79 | rec := httptest.NewRecorder() 80 | c := e.NewContext(req, rec) 81 | 82 | err := echo_endpoint2(c) 83 | 84 | MapStatus(http.StatusBadGateway, func() ProblemDetailErr { 85 | return &ProblemDetail{ 86 | Status: http.StatusUnauthorized, 87 | Title: "unauthorized", 88 | Detail: err.Error(), 89 | } 90 | }) 91 | 92 | p, _ := ResolveProblemDetails(c.Response(), c.Request(), err) 93 | 94 | assert.Equal(t, c.Response().Status, http.StatusUnauthorized) 95 | assert.Equal(t, err.(*echo.HTTPError).Message.(error).Error(), p.GetDetails()) 96 | assert.Equal(t, "unauthorized", p.GetTitle()) 97 | assert.Equal(t, "https://httpstatuses.io/401", p.GetType()) 98 | assert.Equal(t, http.StatusUnauthorized, p.GetStatus()) 99 | } 100 | 101 | func TestMap_Unhandled_Err_Echo(t *testing.T) { 102 | 103 | e := echo.New() 104 | req := httptest.NewRequest(http.MethodGet, "http://echo_endpoint3", nil) 105 | rec := httptest.NewRecorder() 106 | c := e.NewContext(req, rec) 107 | 108 | err := echo_endpoint3(c) 109 | 110 | p, _ := ResolveProblemDetails(c.Response(), c.Request(), err) 111 | 112 | assert.Equal(t, c.Response().Status, http.StatusInternalServerError) 113 | assert.Equal(t, err.Error(), p.GetDetails()) 114 | assert.Equal(t, "Internal Server Error", p.GetTitle()) 115 | assert.Equal(t, "https://httpstatuses.io/500", p.GetType()) 116 | assert.Equal(t, http.StatusInternalServerError, p.GetStatus()) 117 | } 118 | 119 | func TestMap_CustomType_Gin(t *testing.T) { 120 | 121 | gin.SetMode(gin.TestMode) 122 | w := httptest.NewRecorder() 123 | c, _ := gin.CreateTestContext(w) 124 | r := gin.Default() 125 | 126 | r.GET("/gin_endpoint1", func(ctx *gin.Context) { 127 | err := errors.New("We have a custom type error in our endpoint") 128 | customBadRequestError := custom_errors.BadRequestError{InternalError: err} 129 | _ = c.Error(customBadRequestError) 130 | }) 131 | 132 | req, _ := http.NewRequest(http.MethodGet, "/gin_endpoint1", nil) 133 | r.ServeHTTP(w, req) 134 | 135 | for _, err := range c.Errors { 136 | 137 | Map[custom_errors.BadRequestError](func() ProblemDetailErr { 138 | return &ProblemDetail{ 139 | Status: http.StatusBadRequest, 140 | Title: "bad-request", 141 | Detail: err.Error(), 142 | } 143 | }) 144 | 145 | p, _ := ResolveProblemDetails(w, req, err) 146 | 147 | assert.Equal(t, http.StatusBadRequest, p.GetStatus()) 148 | assert.Equal(t, err.Error(), p.GetDetails()) 149 | assert.Equal(t, "bad-request", p.GetTitle()) 150 | assert.Equal(t, "https://httpstatuses.io/400", p.GetType()) 151 | } 152 | } 153 | 154 | func TestMap_Custom_Problem_Err_Gin(t *testing.T) { 155 | 156 | gin.SetMode(gin.TestMode) 157 | w := httptest.NewRecorder() 158 | c, _ := gin.CreateTestContext(w) 159 | r := gin.Default() 160 | 161 | r.GET("/gin_endpoint4", func(ctx *gin.Context) { 162 | err := errors.New("We have a custom error with custom problem details error in our endpoint") 163 | customConflictError := custom_errors.ConflictError{InternalError: err} 164 | _ = c.Error(customConflictError) 165 | }) 166 | 167 | req, _ := http.NewRequest(http.MethodGet, "/gin_endpoint4", nil) 168 | r.ServeHTTP(w, req) 169 | 170 | for _, err := range c.Errors { 171 | 172 | Map[custom_errors.ConflictError](func() ProblemDetailErr { 173 | return &CustomProblemDetailTest{ 174 | ProblemDetailErr: &ProblemDetail{ 175 | Status: http.StatusConflict, 176 | Title: "conflict", 177 | Detail: err.Error(), 178 | }, 179 | AdditionalInfo: "some additional info...", 180 | Description: "some description...", 181 | } 182 | }) 183 | 184 | p, _ := ResolveProblemDetails(w, req, err) 185 | cp := p.(*CustomProblemDetailTest) 186 | 187 | assert.Equal(t, http.StatusConflict, cp.GetStatus()) 188 | assert.Equal(t, err.Error(), cp.GetDetails()) 189 | assert.Equal(t, "conflict", cp.GetTitle()) 190 | assert.Equal(t, "https://httpstatuses.io/409", cp.GetType()) 191 | assert.Equal(t, "some description...", cp.Description) 192 | assert.Equal(t, "some additional info...", cp.AdditionalInfo) 193 | } 194 | } 195 | 196 | func TestMap_Status_Gin(t *testing.T) { 197 | 198 | gin.SetMode(gin.TestMode) 199 | w := httptest.NewRecorder() 200 | c, _ := gin.CreateTestContext(w) 201 | r := gin.Default() 202 | 203 | r.GET("/gin_endpoint2", func(ctx *gin.Context) { 204 | err := errors.New("We have a specific status code error in our endpoint") 205 | _ = c.AbortWithError(http.StatusBadGateway, err) 206 | }) 207 | 208 | req, _ := http.NewRequest(http.MethodGet, "/gin_endpoint2", nil) 209 | r.ServeHTTP(w, req) 210 | 211 | for _, err := range c.Errors { 212 | 213 | MapStatus(http.StatusBadGateway, func() ProblemDetailErr { 214 | return &ProblemDetail{ 215 | Status: http.StatusUnauthorized, 216 | Title: "unauthorized", 217 | Detail: err.Error(), 218 | } 219 | }) 220 | 221 | p, _ := ResolveProblemDetails(w, req, err) 222 | 223 | assert.Equal(t, http.StatusUnauthorized, p.GetStatus()) 224 | assert.Equal(t, err.Error(), p.GetDetails()) 225 | assert.Equal(t, "unauthorized", p.GetTitle()) 226 | assert.Equal(t, "https://httpstatuses.io/401", p.GetType()) 227 | } 228 | } 229 | 230 | func TestMap_Unhandled_Err_Gin(t *testing.T) { 231 | 232 | gin.SetMode(gin.TestMode) 233 | w := httptest.NewRecorder() 234 | c, _ := gin.CreateTestContext(w) 235 | r := gin.Default() 236 | 237 | r.GET("/gin_endpoint3", func(ctx *gin.Context) { 238 | err := errors.New("We have a unhandeled error in our endpoint") 239 | _ = c.Error(err) 240 | }) 241 | 242 | req, _ := http.NewRequest(http.MethodGet, "/gin_endpoint3", nil) 243 | r.ServeHTTP(w, req) 244 | 245 | for _, err := range c.Errors { 246 | 247 | p, _ := ResolveProblemDetails(w, req, err) 248 | 249 | assert.Equal(t, http.StatusInternalServerError, p.GetStatus()) 250 | assert.Equal(t, err.Error(), p.GetDetails()) 251 | assert.Equal(t, "Internal Server Error", p.GetTitle()) 252 | assert.Equal(t, "https://httpstatuses.io/500", p.GetType()) 253 | } 254 | } 255 | 256 | func TestMap_CustomType_Fiber(t *testing.T) { 257 | app := fiber.New() 258 | 259 | app.Get("/fiber_endpoint1", func(c fiber.Ctx) error { 260 | return fiber_endpoint1(c) 261 | }) 262 | 263 | // Create fasthttp request context 264 | fctx := &fasthttp.RequestCtx{} 265 | fctx.Request.SetRequestURI("/fiber_endpoint1") 266 | fctx.Request.Header.SetMethod(http.MethodGet) 267 | 268 | // Create Fiber context 269 | ctx := app.AcquireCtx(fctx) 270 | defer app.ReleaseCtx(ctx) 271 | 272 | // Execute the handler 273 | handlerErr := fiber_endpoint1(ctx) 274 | 275 | Map[custom_errors.BadRequestError](func() ProblemDetailErr { 276 | return &ProblemDetail{ 277 | Status: http.StatusBadRequest, 278 | Title: "bad-request", 279 | Detail: handlerErr.Error(), 280 | } 281 | }) 282 | 283 | p, _ := ResolveProblemDetails(Response(ctx), Request(ctx), handlerErr) 284 | 285 | assert.Equal(t, http.StatusBadRequest, ctx.Response().StatusCode()) 286 | assert.Equal(t, handlerErr.Error(), p.GetDetails()) 287 | assert.Equal(t, "bad-request", p.GetTitle()) 288 | assert.Equal(t, "https://httpstatuses.io/400", p.GetType()) 289 | assert.Equal(t, http.StatusBadRequest, p.GetStatus()) 290 | } 291 | 292 | func TestMap_Custom_Problem_Err_Fiber(t *testing.T) { 293 | app := fiber.New() 294 | 295 | app.Get("/fiber_endpoint4", func(c fiber.Ctx) error { 296 | return fiber_endpoint4(c) 297 | }) 298 | 299 | fctx := &fasthttp.RequestCtx{} 300 | fctx.Request.SetRequestURI("/fiber_endpoint4") 301 | fctx.Request.Header.SetMethod(http.MethodGet) 302 | 303 | ctx := app.AcquireCtx(fctx) 304 | defer app.ReleaseCtx(ctx) 305 | 306 | handlerErr := fiber_endpoint4(ctx) 307 | 308 | Map[custom_errors.ConflictError](func() ProblemDetailErr { 309 | return &CustomProblemDetailTest{ 310 | ProblemDetailErr: &ProblemDetail{ 311 | Status: http.StatusConflict, 312 | Title: "conflict", 313 | Detail: handlerErr.Error(), 314 | }, 315 | AdditionalInfo: "some additional info...", 316 | Description: "some description...", 317 | } 318 | }) 319 | 320 | p, _ := ResolveProblemDetails(Response(ctx), Request(ctx), handlerErr) 321 | cp := p.(*CustomProblemDetailTest) 322 | 323 | assert.Equal(t, http.StatusConflict, ctx.Response().StatusCode()) 324 | assert.Equal(t, handlerErr.Error(), cp.GetDetails()) 325 | assert.Equal(t, "conflict", cp.GetTitle()) 326 | assert.Equal(t, "https://httpstatuses.io/409", cp.GetType()) 327 | assert.Equal(t, http.StatusConflict, cp.GetStatus()) 328 | assert.Equal(t, "some description...", cp.Description) 329 | assert.Equal(t, "some additional info...", cp.AdditionalInfo) 330 | } 331 | 332 | func TestMap_Status_Fiber(t *testing.T) { 333 | app := fiber.New() 334 | 335 | app.Get("/fiber_endpoint2", func(c fiber.Ctx) error { 336 | return fiber_endpoint2(c) 337 | }) 338 | 339 | fctx := &fasthttp.RequestCtx{} 340 | fctx.Request.SetRequestURI("/fiber_endpoint2") 341 | fctx.Request.Header.SetMethod(http.MethodGet) 342 | 343 | ctx := app.AcquireCtx(fctx) 344 | defer app.ReleaseCtx(ctx) 345 | 346 | handlerErr := fiber_endpoint2(ctx) 347 | 348 | MapStatus(http.StatusBadGateway, func() ProblemDetailErr { 349 | return &ProblemDetail{ 350 | Status: http.StatusUnauthorized, 351 | Title: "unauthorized", 352 | Detail: handlerErr.Error(), 353 | } 354 | }) 355 | 356 | p, _ := ResolveProblemDetails(Response(ctx), Request(ctx), handlerErr) 357 | 358 | assert.Equal(t, http.StatusUnauthorized, ctx.Response().StatusCode()) 359 | assert.Equal(t, handlerErr.Error(), p.GetDetails()) 360 | assert.Equal(t, "unauthorized", p.GetTitle()) 361 | assert.Equal(t, "https://httpstatuses.io/401", p.GetType()) 362 | assert.Equal(t, http.StatusUnauthorized, p.GetStatus()) 363 | } 364 | 365 | func TestMap_Unhandled_Err_Fiber(t *testing.T) { 366 | app := fiber.New() 367 | 368 | app.Get("/fiber_endpoint3", func(c fiber.Ctx) error { 369 | return fiber_endpoint3(c) 370 | }) 371 | 372 | fctx := &fasthttp.RequestCtx{} 373 | fctx.Request.SetRequestURI("/fiber_endpoint3") 374 | fctx.Request.Header.SetMethod(http.MethodGet) 375 | 376 | ctx := app.AcquireCtx(fctx) 377 | defer app.ReleaseCtx(ctx) 378 | 379 | handlerErr := fiber_endpoint3(ctx) 380 | 381 | p, _ := ResolveProblemDetails(Response(ctx), Request(ctx), handlerErr) 382 | 383 | assert.Equal(t, http.StatusInternalServerError, ctx.Response().StatusCode()) 384 | assert.Equal(t, handlerErr.Error(), p.GetDetails()) 385 | assert.Equal(t, "Internal Server Error", p.GetTitle()) 386 | assert.Equal(t, "https://httpstatuses.io/500", p.GetType()) 387 | assert.Equal(t, http.StatusInternalServerError, p.GetStatus()) 388 | } 389 | 390 | func echo_endpoint1(c echo.Context) error { 391 | err := errors.New("We have a custom type error in our endpoint") 392 | return custom_errors.BadRequestError{InternalError: err} 393 | } 394 | 395 | func echo_endpoint2(c echo.Context) error { 396 | err := errors.New("We have a specific status code error in our endpoint") 397 | return echo.NewHTTPError(http.StatusBadGateway, err) 398 | } 399 | 400 | func echo_endpoint3(c echo.Context) error { 401 | err := errors.New("We have a unhandeled error in our endpoint") 402 | return err 403 | } 404 | 405 | func echo_endpoint4(c echo.Context) error { 406 | err := errors.New("We have a custom error with custom problem details error in our endpoint") 407 | return custom_errors.ConflictError{InternalError: err} 408 | } 409 | 410 | func fiber_endpoint1(c fiber.Ctx) error { 411 | err := errors.New("We have a custom type error in our endpoint") 412 | return custom_errors.BadRequestError{InternalError: err} 413 | } 414 | 415 | func fiber_endpoint2(c fiber.Ctx) error { 416 | err := errors.New("We have a specific status code error in our endpoint") 417 | return fiber.NewError(http.StatusBadGateway, err.Error()) 418 | } 419 | 420 | func fiber_endpoint3(c fiber.Ctx) error { 421 | err := errors.New("We have an unhandled error in our endpoint") 422 | return err 423 | } 424 | 425 | func fiber_endpoint4(c fiber.Ctx) error { 426 | err := errors.New("We have a custom error with custom problem details error in our endpoint") 427 | return custom_errors.ConflictError{InternalError: err} 428 | } 429 | 430 | type CustomProblemDetailTest struct { 431 | ProblemDetailErr 432 | Description string `json:"description,omitempty"` 433 | AdditionalInfo string `json:"additionalInfo,omitempty"` 434 | } 435 | -------------------------------------------------------------------------------- /samples/cmd/echo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/labstack/echo/v4" 5 | "github.com/labstack/gommon/log" 6 | "github.com/meysamhadeli/problem-details" 7 | "github.com/meysamhadeli/problem-details/samples/custom-errors" 8 | custom_problems "github.com/meysamhadeli/problem-details/samples/custom-problems" 9 | "github.com/pkg/errors" 10 | "net/http" 11 | ) 12 | 13 | func main() { 14 | e := echo.New() 15 | 16 | e.HTTPErrorHandler = EchoErrorHandler 17 | 18 | e.GET("/sample1", sample1) 19 | e.GET("/sample2", sample2) 20 | e.GET("/sample3", sample3) 21 | 22 | e.Logger.Fatal(e.Start(":3000")) 23 | } 24 | 25 | // handle specific status code to problem details error 26 | func sample1(c echo.Context) error { 27 | err := errors.New("We have a specific status code error in our endpoint") 28 | // change status code 'StatusBadGateway' to 'StatusUnauthorized' base on handler config 29 | return echo.NewHTTPError(http.StatusBadGateway, err) 30 | } 31 | 32 | // handle custom type error to problem details error 33 | func sample2(c echo.Context) error { 34 | err := errors.New("We have a custom type error in our endpoint") 35 | return custom_errors.BadRequestError{InternalError: err} 36 | } 37 | 38 | // handle custom type error to custom problem details error 39 | func sample3(c echo.Context) error { 40 | err := errors.New("We have a custom error with custom problem details error in our endpoint") 41 | return custom_errors.ConflictError{InternalError: err} 42 | } 43 | 44 | // EchoErrorHandler middleware for handle problem details error on echo 45 | func EchoErrorHandler(error error, c echo.Context) { 46 | 47 | // map custom type error to problem details error 48 | problem.Map[custom_errors.BadRequestError](func() problem.ProblemDetailErr { 49 | return &problem.ProblemDetail{ 50 | Status: http.StatusBadRequest, 51 | Title: "bad request", 52 | Detail: error.Error(), 53 | } 54 | }) 55 | 56 | // map custom type error to custom problem details error 57 | problem.Map[custom_errors.ConflictError](func() problem.ProblemDetailErr { 58 | return &custom_problems.CustomProblemDetail{ 59 | ProblemDetailErr: &problem.ProblemDetail{ 60 | Status: http.StatusConflict, 61 | Title: "conflict", 62 | Detail: error.Error(), 63 | }, 64 | AdditionalInfo: "some additional info...", 65 | Description: "some description...", 66 | } 67 | }) 68 | 69 | // map status code to problem details error 70 | problem.MapStatus(http.StatusBadGateway, func() problem.ProblemDetailErr { 71 | return &problem.ProblemDetail{ 72 | Status: http.StatusUnauthorized, 73 | Title: "unauthorized", 74 | Detail: error.Error(), 75 | } 76 | }) 77 | 78 | // resolve problem details error from response in echo 79 | if !c.Response().Committed { 80 | if _, err := problem.ResolveProblemDetails(c.Response(), c.Request(), error); err != nil { 81 | log.Error(err) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /samples/cmd/fiber/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gofiber/fiber/v3" 5 | "github.com/labstack/gommon/log" 6 | "github.com/meysamhadeli/problem-details" 7 | "github.com/meysamhadeli/problem-details/samples/custom-errors" 8 | custom_problems "github.com/meysamhadeli/problem-details/samples/custom-problems" 9 | "github.com/pkg/errors" 10 | "net/http" 11 | ) 12 | 13 | func main() { 14 | app := fiber.New() 15 | 16 | // Register error handler middleware 17 | app.Use(FiberErrorHandler) 18 | 19 | app.Get("/sample1", sample1) 20 | app.Get("/sample2", sample2) 21 | app.Get("/sample3", sample3) 22 | 23 | log.Fatal(app.Listen(":3000")) 24 | } 25 | 26 | // handle specific status code to problem details error 27 | func sample1(c fiber.Ctx) error { 28 | err := errors.New("We have a specific status code error in our endpoint") 29 | // change status code 'StatusBadGateway' to 'StatusUnauthorized' base on handler config 30 | return fiber.NewError(http.StatusBadGateway, err.Error()) 31 | } 32 | 33 | // handle custom type error to problem details error 34 | func sample2(c fiber.Ctx) error { 35 | err := errors.New("We have a custom type error in our endpoint") 36 | return custom_errors.BadRequestError{InternalError: err} 37 | } 38 | 39 | // handle custom type error to custom problem details error 40 | func sample3(c fiber.Ctx) error { 41 | err := errors.New("We have a custom error with custom problem details error in our endpoint") 42 | return custom_errors.ConflictError{InternalError: err} 43 | } 44 | 45 | // FiberErrorHandler middleware for handling problem details error on Fiber 46 | func FiberErrorHandler(c fiber.Ctx) error { 47 | err := c.Next() 48 | 49 | if err != nil { 50 | // map custom type error to problem details error 51 | problem.Map[custom_errors.BadRequestError](func() problem.ProblemDetailErr { 52 | return &problem.ProblemDetail{ 53 | Status: http.StatusBadRequest, 54 | Title: "bad request", 55 | Detail: err.Error(), 56 | } 57 | }) 58 | 59 | // map custom type error to custom problem details error 60 | problem.Map[custom_errors.ConflictError](func() problem.ProblemDetailErr { 61 | return &custom_problems.CustomProblemDetail{ 62 | ProblemDetailErr: &problem.ProblemDetail{ 63 | Status: http.StatusConflict, 64 | Title: "conflict", 65 | Detail: err.Error(), 66 | }, 67 | AdditionalInfo: "some additional info...", 68 | Description: "some description...", 69 | } 70 | }) 71 | 72 | // map status code to problem details error 73 | problem.MapStatus(http.StatusBadGateway, func() problem.ProblemDetailErr { 74 | return &problem.ProblemDetail{ 75 | Status: http.StatusUnauthorized, 76 | Title: "unauthorized", 77 | Detail: err.Error(), 78 | } 79 | }) 80 | 81 | // resolve problem details error 82 | if _, err := problem.ResolveProblemDetails(problem.Response(c), problem.Request(c), err); err != nil { 83 | log.Error(err) 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /samples/cmd/gin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/labstack/gommon/log" 6 | "github.com/meysamhadeli/problem-details" 7 | custom_errors "github.com/meysamhadeli/problem-details/samples/custom-errors" 8 | custom_problems "github.com/meysamhadeli/problem-details/samples/custom-problems" 9 | "github.com/pkg/errors" 10 | "net/http" 11 | ) 12 | 13 | func main() { 14 | 15 | r := gin.Default() 16 | 17 | r.Use(GinErrorHandler()) 18 | 19 | r.GET("/sample1", sample1) 20 | r.GET("/sample2", sample2) 21 | r.GET("/sample3", sample3) 22 | 23 | r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") 24 | } 25 | 26 | // handle specific status code to problem details error 27 | func sample1(c *gin.Context) { 28 | err := errors.New("We have a specific status code error in our endpoint") 29 | // change status code 'StatusBadGateway' to 'StatusUnauthorized' base on handler config 30 | _ = c.AbortWithError(http.StatusBadGateway, err) 31 | } 32 | 33 | // handle custom type error to problem details error 34 | func sample2(c *gin.Context) { 35 | 36 | err := errors.New("We have a custom type error in our endpoint") 37 | customBadRequestError := custom_errors.BadRequestError{InternalError: err} 38 | _ = c.Error(customBadRequestError) 39 | } 40 | 41 | // handle custom type error to custom problem details error 42 | func sample3(c *gin.Context) { 43 | err := errors.New("We have a custom error with custom problem details error in our endpoint") 44 | customConflictError := custom_errors.ConflictError{InternalError: err} 45 | _ = c.Error(customConflictError) 46 | } 47 | 48 | // GinErrorHandler middleware for handle problem details error on gin 49 | func GinErrorHandler() gin.HandlerFunc { 50 | return func(c *gin.Context) { 51 | 52 | c.Next() 53 | 54 | for _, err := range c.Errors { 55 | 56 | // map custom type error to problem details error 57 | problem.Map[custom_errors.BadRequestError](func() problem.ProblemDetailErr { 58 | return &problem.ProblemDetail{ 59 | Status: http.StatusBadRequest, 60 | Title: "bad request", 61 | Detail: err.Error(), 62 | } 63 | }) 64 | 65 | // map custom type error to custom problem details error 66 | problem.Map[custom_errors.ConflictError](func() problem.ProblemDetailErr { 67 | return &custom_problems.CustomProblemDetail{ 68 | ProblemDetailErr: &problem.ProblemDetail{ 69 | Status: http.StatusConflict, 70 | Title: "conflict", 71 | Detail: err.Error(), 72 | }, 73 | AdditionalInfo: "some additional info...", 74 | Description: "some description...", 75 | } 76 | }) 77 | 78 | // map status code to problem details error 79 | problem.MapStatus(http.StatusBadGateway, func() problem.ProblemDetailErr { 80 | return &problem.ProblemDetail{ 81 | Status: http.StatusUnauthorized, 82 | Title: "unauthorized", 83 | Detail: err.Error(), 84 | } 85 | }) 86 | 87 | if _, err := problem.ResolveProblemDetails(c.Writer, c.Request, err); err != nil { 88 | log.Error(err) 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /samples/custom-errors/custom-error.go: -------------------------------------------------------------------------------- 1 | package custom_errors 2 | 3 | type BadRequestError struct { 4 | InternalError error 5 | } 6 | 7 | type ConflictError struct { 8 | InternalError error 9 | } 10 | 11 | func (c ConflictError) Error() string { 12 | return c.InternalError.Error() 13 | } 14 | 15 | func (b BadRequestError) Error() string { 16 | return b.InternalError.Error() 17 | } 18 | -------------------------------------------------------------------------------- /samples/custom-problems/custom-problem.go: -------------------------------------------------------------------------------- 1 | package custom_problems 2 | 3 | import "github.com/meysamhadeli/problem-details" 4 | 5 | type CustomProblemDetail struct { 6 | problem.ProblemDetailErr 7 | Description string `json:"description,omitempty"` 8 | AdditionalInfo string `json:"additionalInfo,omitempty"` 9 | } 10 | --------------------------------------------------------------------------------