From 19f737d2404e5a9516e793fb10940f30d821d726 Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Wed, 20 Nov 2024 12:02:35 -0500 Subject: [PATCH 1/4] feat: add ErrorWithDetail --- errors.go | 25 ++++++++++++++++++++----- errors_test.go | 23 ++++++++++++++++++++--- examples/basic/main.go | 20 ++++++++++++++++++++ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/errors.go b/errors.go index 75ffcb07..d4adaf41 100644 --- a/errors.go +++ b/errors.go @@ -14,6 +14,11 @@ type ErrorWithStatus interface { StatusCode() int } +type ErrorWithDetail interface { + error + DetailMsg() string +} + // HTTPError is the error response used by the serialization part of the framework. type HTTPError struct { Err error `json:"-" xml:"-"` // Developer readable error message. Not shown to the user to avoid security leaks. @@ -40,7 +45,7 @@ func (e HTTPError) Error() string { title = "HTTP Error" } } - return fmt.Sprintf("%d %s: %s", code, title, e.Detail) + return fmt.Sprintf("%d %s: %s", code, title, e.DetailMsg()) } func (e HTTPError) StatusCode() int { @@ -50,6 +55,10 @@ func (e HTTPError) StatusCode() int { return e.Status } +func (e HTTPError) DetailMsg() string { + return e.Detail +} + func (e HTTPError) Unwrap() error { return e.Err } // BadRequestError is an error used to return a 400 status code. @@ -122,11 +131,11 @@ func (e NotAcceptableError) StatusCode() int { return http.StatusNotAcceptable } func (e NotAcceptableError) Unwrap() error { return HTTPError(e) } // ErrorHandler is the default error handler used by the framework. -// If the error is an [HTTPError] that is error is returned. -// If the error adheres to the [ErrorWithStatus] interface +// If the error is an [HTTPError] that error is returned. +// If the error adheres to the [ErrorWithStatus] and/or [ErrorWithDetail] interface // the error is transformed to a [HTTPError]. // If the error is not an [HTTPError] nor does it adhere to an -// interface the error is returned. +// interface the error is returned as is. func ErrorHandler(err error) error { var errorStatus ErrorWithStatus switch { @@ -154,11 +163,17 @@ func handleHTTPError(err error) HTTPError { errResponse.Status = errorStatus.StatusCode() } + // Check for detail + var errorDetail ErrorWithDetail + if errors.As(err, &errorDetail) { + errResponse.Detail = errorDetail.DetailMsg() + } + if errResponse.Title == "" { errResponse.Title = http.StatusText(errResponse.Status) } - slog.Error("Error "+errResponse.Title, "status", errResponse.StatusCode(), "detail", errResponse.Detail, "error", errResponse.Err) + slog.Error("Error "+errResponse.Title, "status", errResponse.StatusCode(), "detail", errResponse.DetailMsg(), "error", errResponse.Err) return errResponse } diff --git a/errors_test.go b/errors_test.go index b49c9a77..c8f65da1 100644 --- a/errors_test.go +++ b/errors_test.go @@ -10,12 +10,17 @@ import ( type myError struct { status int + err HTTPError + detail string } var _ ErrorWithStatus = myError{} +var _ ErrorWithDetail = myError{} -func (e myError) Error() string { return "test error" } -func (e myError) StatusCode() int { return e.status } +func (e myError) Error() string { return "test error" } +func (e myError) StatusCode() int { return e.status } +func (e myError) DetailMsg() string { return e.detail } +func (e myError) Unwrap() error { return HTTPError(e.err) } func TestErrorHandler(t *testing.T) { t.Run("basic", func(t *testing.T) { @@ -49,7 +54,7 @@ func TestErrorHandler(t *testing.T) { require.ErrorContains(t, err, "Internal Server Error") }) - t.Run("error with status ", func(t *testing.T) { + t.Run("error with status", func(t *testing.T) { err := myError{ status: http.StatusNotFound, } @@ -60,6 +65,18 @@ func TestErrorHandler(t *testing.T) { require.Equal(t, http.StatusNotFound, errResponse.(HTTPError).StatusCode()) }) + t.Run("error with detail", func(t *testing.T) { + err := myError{ + detail: "my detail", + } + errResponse := ErrorHandler(err) + require.ErrorAs(t, errResponse, &HTTPError{}) + require.Contains(t, errResponse.Error(), "Internal Server Error") + require.Contains(t, errResponse.Error(), "500") + require.Contains(t, errResponse.Error(), "my detail") + require.Equal(t, http.StatusInternalServerError, errResponse.(HTTPError).StatusCode()) + }) + t.Run("conflict error", func(t *testing.T) { err := ConflictError{ Err: errors.New("Conflict"), diff --git a/examples/basic/main.go b/examples/basic/main.go index 84833dc8..5ae6a49a 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -22,6 +22,22 @@ type MyResponse struct { BestFramework string `json:"best"` } +type MyError struct { + Err error `json:"error"` + Message string `json:"message"` +} + +var _ fuego.ErrorWithStatus = MyError{} +var _ fuego.ErrorWithDetail = MyError{} + +func (e MyError) Error() string { return e.Err.Error() } + +func (e MyError) StatusCode() int { return http.StatusTeapot } + +func (e MyError) DetailMsg() string { + return strings.Split(e.Error(), " ")[1] +} + func main() { s := fuego.NewServer( fuego.WithAddr("localhost:8088"), @@ -59,6 +75,10 @@ func main() { w.Write([]byte("Hello, World!")) }) + fuego.Get(s, "/custom-err", func(c *fuego.ContextNoBody) (string, error) { + return "hello", MyError{Err: errors.New("my error")} + }) + s.Run() } From ec6c6c89aa11f0c2138a6e07e4379f828058000d Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Fri, 6 Dec 2024 13:55:29 -0500 Subject: [PATCH 2/4] chore: update error handling docs --- documentation/docs/guides/errors.md | 80 ++++++++++++++++++++++++++--- errors.go | 19 ++++--- server.go | 1 + 3 files changed, 86 insertions(+), 14 deletions(-) diff --git a/documentation/docs/guides/errors.md b/documentation/docs/guides/errors.md index 98eb8e85..a163eaf7 100644 --- a/documentation/docs/guides/errors.md +++ b/documentation/docs/guides/errors.md @@ -4,22 +4,85 @@ Error handling is a crucial part of any application. It is important to handle e ## Error handling in Fuego -Fuego [controllers](./controllers) returns a value and an error. If the error is not `nil`, it means that an error occurred while processing the request. The error will be returned to the client as a JSON response. +Fuego [controllers](./controllers) returns a value and an error. If the error is not `nil`, +it means that an error occurred while processing the request. +The error will be returned to the client as a JSON or XML response. -By default, Fuego implements [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457), which defines a standard error format for HTTP APIs. We strongly recommend following this standard, but you can also use your own errors. +The default error handler will transform any error that implements the +`fuego.ErrorWithStatus` or `fuego.ErrorWithDetail` interfaces into a `fuego.HTTPError`. The `fuego.HTTPError` implements +[RFC 9457](https://www.rfc-editor.org/rfc/rfc9457), which defines a standard error format for HTTP APIs. +We strongly recommend following this standard, but you can also use your own errors. -The error type returned as JSON is `fuego.HTTPError`. It has a `Status` and a `Info` field. The `Status` field is an integer that represents the error code. The `Info` field is a string that contains a human-readable error message. +The default `fuego.ErrorHandler` can be overridden using `fuego.WithErrorHandler` at fuego Server creation time. -If your error implements `Status() int` and `Info()` methods, the error will include the status code and the error message in the `fuego.HTTPError` response. +The error type of `fuego.HTTPError` is returned as JSON or XML depending on the content-type specified. +It's structure is the following: + +```go +// HTTPError is the error response used by the serialization part of the framework. +type HTTPError struct { + // Developer readable error message. Not shown to the user to avoid security leaks. + Err error `json:"-" xml:"-"` + // URL of the error type. Can be used to lookup the error in a documentation + Type string `json:"type,omitempty" xml:"type,omitempty" description:"URL of the error type. Can be used to lookup the error in a documentation"` + // Short title of the error + Title string `json:"title,omitempty" xml:"title,omitempty" description:"Short title of the error"` + // HTTP status code. If using a different type than [HTTPError], for example [BadRequestError], + // this will be automatically overridden after Fuego error handling. + Status int `json:"status,omitempty" xml:"status,omitempty" description:"HTTP status code" example:"403"` + // Human readable error message + Detail string `json:"detail,omitempty" xml:"detail,omitempty" description:"Human readable error message"` + Instance string `json:"instance,omitempty" xml:"instance,omitempty"` + Errors []ErrorItem `json:"errors,omitempty" xml:"errors,omitempty"` +} +``` + +If your error implements `fuego.ErrorWithStatus` or `fuego.ErrorWithDetail` +the error will be returned as a `fuego.HTTPError`. + +```go +// ErrorWithStatus is an interface that can be implemented by an error to provide +// a status code +type ErrorWithStatus interface { + error + StatusCode() int +} + +// ErrorWithDetail is an interface that can be implemented by an error to provide +// an additional detail message about the error +type ErrorWithDetail interface { + error + DetailMsg() string +} +``` + +Example: ```go type MyCustomError struct { - Status int - Message string + Err error `json:"error"` + Message string `json:"message"` +} + +var _ fuego.ErrorWithStatus = MyCustomError{} +var _ fuego.ErrorWithDetail = MyCustomError{} + +func (e MyCustomError) Error() string { return e.Err.Error() } + +func (e MyCustomError) StatusCode() int { return http.StatusTeapot } + +func (e MyCustomError) DetailMsg() string { + return strings.Split(e.Error(), " ")[1] } +``` -func (e MyCustomError) Status() int { - return e.Status +Alternatively, you can always use `fuego.HTTPError` directly such as: + +```go +err := fuego.HTTPError{ + Title: "unauthorized access", + Detail: "wrong username or password", + Status: http.StatusUnauthorized, } ``` @@ -33,3 +96,4 @@ Fuego provides a set of default errors that you can use in your application. - `fuego.NotFoundError`: 404 Not Found - `fuego.ConflictError`: 409 Conflict - `fuego.InternalServerError`: 500 Internal Server Error +- `fuego.NotAcceptableError`: 406 Not Acceptable diff --git a/errors.go b/errors.go index d4adaf41..526c0f29 100644 --- a/errors.go +++ b/errors.go @@ -8,12 +8,14 @@ import ( ) // ErrorWithStatus is an interface that can be implemented by an error to provide -// additional information about the error. +// a status code type ErrorWithStatus interface { error StatusCode() int } +// ErrorWithDetail is an interface that can be implemented by an error to provide +// an additional detail message about the error type ErrorWithDetail interface { error DetailMsg() string @@ -21,11 +23,16 @@ type ErrorWithDetail interface { // HTTPError is the error response used by the serialization part of the framework. type HTTPError struct { - Err error `json:"-" xml:"-"` // Developer readable error message. Not shown to the user to avoid security leaks. - Type string `json:"type,omitempty" xml:"type,omitempty" description:"URL of the error type. Can be used to lookup the error in a documentation"` // URL of the error type. Can be used to lookup the error in a documentation - Title string `json:"title,omitempty" xml:"title,omitempty" description:"Short title of the error"` // Short title of the error - Status int `json:"status,omitempty" xml:"status,omitempty" description:"HTTP status code" example:"403"` // HTTP status code. If using a different type than [HTTPError], for example [BadRequestError], this will be automatically overridden after Fuego error handling. - Detail string `json:"detail,omitempty" xml:"detail,omitempty" description:"Human readable error message"` // Human readable error message + // Developer readable error message. Not shown to the user to avoid security leaks. + Err error `json:"-" xml:"-"` + // URL of the error type. Can be used to lookup the error in a documentation + Type string `json:"type,omitempty" xml:"type,omitempty" description:"URL of the error type. Can be used to lookup the error in a documentation"` + // Short title of the error + Title string `json:"title,omitempty" xml:"title,omitempty" description:"Short title of the error"` + // HTTP status code. If using a different type than [HTTPError], for example [BadRequestError], this will be automatically overridden after Fuego error handling. + Status int `json:"status,omitempty" xml:"status,omitempty" description:"HTTP status code" example:"403"` + // Human readable error message + Detail string `json:"detail,omitempty" xml:"detail,omitempty" description:"Human readable error message"` Instance string `json:"instance,omitempty" xml:"instance,omitempty"` Errors []ErrorItem `json:"errors,omitempty" xml:"errors,omitempty"` } diff --git a/server.go b/server.go index 6daba7ee..a0215c23 100644 --- a/server.go +++ b/server.go @@ -370,6 +370,7 @@ func WithErrorSerializer(serializer ErrorSender) func(*Server) { return func(c *Server) { c.SerializeError = serializer } } +// WithErrorHandler sets a customer error handler for the server func WithErrorHandler(errorHandler func(err error) error) func(*Server) { return func(c *Server) { c.ErrorHandler = errorHandler } } From 7b1ef0defb0a797639d92d8b51d799075e270bbe Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Fri, 6 Dec 2024 14:49:03 -0500 Subject: [PATCH 3/4] chore: custom-errors as its own example --- examples/basic/main.go | 20 ---------- examples/custom-errors/go.mod | 32 ++++++++++++++++ examples/custom-errors/go.sum | 69 ++++++++++++++++++++++++++++++++++ examples/custom-errors/main.go | 46 +++++++++++++++++++++++ 4 files changed, 147 insertions(+), 20 deletions(-) create mode 100644 examples/custom-errors/go.mod create mode 100644 examples/custom-errors/go.sum create mode 100644 examples/custom-errors/main.go diff --git a/examples/basic/main.go b/examples/basic/main.go index 5ae6a49a..84833dc8 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -22,22 +22,6 @@ type MyResponse struct { BestFramework string `json:"best"` } -type MyError struct { - Err error `json:"error"` - Message string `json:"message"` -} - -var _ fuego.ErrorWithStatus = MyError{} -var _ fuego.ErrorWithDetail = MyError{} - -func (e MyError) Error() string { return e.Err.Error() } - -func (e MyError) StatusCode() int { return http.StatusTeapot } - -func (e MyError) DetailMsg() string { - return strings.Split(e.Error(), " ")[1] -} - func main() { s := fuego.NewServer( fuego.WithAddr("localhost:8088"), @@ -75,10 +59,6 @@ func main() { w.Write([]byte("Hello, World!")) }) - fuego.Get(s, "/custom-err", func(c *fuego.ContextNoBody) (string, error) { - return "hello", MyError{Err: errors.New("my error")} - }) - s.Run() } diff --git a/examples/custom-errors/go.mod b/examples/custom-errors/go.mod new file mode 100644 index 00000000..d5b869de --- /dev/null +++ b/examples/custom-errors/go.mod @@ -0,0 +1,32 @@ +module github.com/go-fuego/fuego/examples/basic + +go 1.22.2 + +require ( + github.com/go-chi/chi/v5 v5.1.0 + github.com/go-fuego/fuego v0.15.1 + github.com/rs/cors v1.11.1 +) + +require ( + github.com/gabriel-vasile/mimetype v1.4.7 // indirect + github.com/getkin/kin-openapi v0.128.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect + github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/gorilla/schema v1.4.1 // indirect + github.com/invopop/yaml v0.3.1 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect + github.com/perimeterx/marshmallow v1.1.5 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/examples/custom-errors/go.sum b/examples/custom-errors/go.sum new file mode 100644 index 00000000..6936a3f1 --- /dev/null +++ b/examples/custom-errors/go.sum @@ -0,0 +1,69 @@ +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.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= +github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +github.com/getkin/kin-openapi v0.128.0 h1:jqq3D9vC9pPq1dGcOCv7yOp1DaEe7c/T1vzcLbITSp4= +github.com/getkin/kin-openapi v0.128.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-fuego/fuego v0.15.1 h1:9ESspiRFeUmSl2YdZzL5VE+8F2sGYSTBRkGJEfvoLXc= +github.com/go-fuego/fuego v0.15.1/go.mod h1:XMpAQQnZsoMON0UD6ncvzj6YukGQlNECDcINJt49zx4= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= +github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= +github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= +github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= +github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso= +github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= +github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= +github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= +github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX24kM= +github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/examples/custom-errors/main.go b/examples/custom-errors/main.go new file mode 100644 index 00000000..268714e7 --- /dev/null +++ b/examples/custom-errors/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "errors" + "net/http" + "strings" + + chiMiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/rs/cors" + + "github.com/go-fuego/fuego" + "github.com/go-fuego/fuego/option" +) + +type MyError struct { + Err error `json:"error"` + Message string `json:"message"` +} + +var _ fuego.ErrorWithStatus = MyError{} +var _ fuego.ErrorWithDetail = MyError{} + +func (e MyError) Error() string { return e.Err.Error() } + +func (e MyError) StatusCode() int { return http.StatusTeapot } + +func (e MyError) DetailMsg() string { + return strings.Split(e.Error(), " ")[1] +} + +func main() { + s := fuego.NewServer( + fuego.WithAddr("localhost:8088"), + ) + + fuego.Use(s, cors.Default().Handler) + fuego.Use(s, chiMiddleware.Compress(5, "text/html", "text/css")) + + fuego.Get(s, "/custom-err", func(c *fuego.ContextNoBody) (string, error) { + return "hello", MyError{Err: errors.New("my error")} + }, + option.AddError(http.StatusTeapot, "my custom teapot error", MyError{}), + ) + + s.Run() +} From 3ab8b64ded38f2da7193d4367b8e101cdfa97b01 Mon Sep 17 00:00:00 2001 From: dylanhitt Date: Fri, 6 Dec 2024 19:06:15 -0500 Subject: [PATCH 4/4] chore: go work for custom-errors --- examples/custom-errors/go.mod | 2 +- go.work | 1 + middleware/cache/go.mod | 8 ++++---- middleware/cache/go.sum | 12 ++++-------- testing-from-outside/go.mod | 6 +++--- testing-from-outside/go.sum | 9 +++------ 6 files changed, 16 insertions(+), 22 deletions(-) diff --git a/examples/custom-errors/go.mod b/examples/custom-errors/go.mod index d5b869de..6b645fc2 100644 --- a/examples/custom-errors/go.mod +++ b/examples/custom-errors/go.mod @@ -1,4 +1,4 @@ -module github.com/go-fuego/fuego/examples/basic +module github.com/go-fuego/fuego/examples/custom-errors go 1.22.2 diff --git a/go.work b/go.work index 0d2d1f60..d6686f05 100644 --- a/go.work +++ b/go.work @@ -5,6 +5,7 @@ use ( ./cmd/fuego ./examples/acme-tls ./examples/basic + ./examples/custom-errors ./examples/custom-serializer ./examples/full-app-gourmet ./examples/generate-opengraph-image diff --git a/middleware/cache/go.mod b/middleware/cache/go.mod index 4324829b..ff325c4c 100644 --- a/middleware/cache/go.mod +++ b/middleware/cache/go.mod @@ -26,9 +26,9 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.20.0 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/middleware/cache/go.sum b/middleware/cache/go.sum index 455cd44a..1955bf17 100644 --- a/middleware/cache/go.sum +++ b/middleware/cache/go.sum @@ -52,14 +52,10 @@ github.com/thejerf/slogassert v0.3.4 h1:VoTsXixRbXMrRSSxDjYTiEDCM4VWbsYPW5rB/hX2 github.com/thejerf/slogassert v0.3.4/go.mod h1:0zn9ISLVKo1aPMTqcGfG1o6dWwt+Rk574GlUxHD4rs8= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/testing-from-outside/go.mod b/testing-from-outside/go.mod index ec10aa5d..092cc79c 100644 --- a/testing-from-outside/go.mod +++ b/testing-from-outside/go.mod @@ -27,9 +27,9 @@ require ( github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.29.0 // indirect - golang.org/x/net v0.31.0 // indirect - golang.org/x/sys v0.27.0 // indirect + golang.org/x/crypto v0.30.0 // indirect + golang.org/x/net v0.32.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/testing-from-outside/go.sum b/testing-from-outside/go.sum index 0d195b30..242c29f1 100644 --- a/testing-from-outside/go.sum +++ b/testing-from-outside/go.sum @@ -52,12 +52,9 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= -golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= -golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= +golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=