diff --git a/error.go b/error.go index 67017c29..70a6b5d6 100644 --- a/error.go +++ b/error.go @@ -248,6 +248,43 @@ var NewError = func(status int, msg string, errs ...error) StatusError { } } +// HandleTypedError dynamically creates an HTTP error response based on the type of the provided error. +// This allows API implementers to customize the HTTP error response according to different error types +// handled by the application. +// +// Usage example: +// +// type NotFoundError struct { +// Resource string +// } +// +// func (e *NotFoundError) Error() string { +// return fmt.Sprintf("Resource '%s' not found", e.Resource) +// } +// +// type ValidationError struct { +// Field string +// Message string +// } +// +// func (e *ValidationError) Error() string { +// return fmt.Sprintf("Validation failed on '%s': %s", e.Field, e.Message) +// } +// +// NewTypedError = func(err error) StatusError { +// switch e := err.(type) { +// case *NotFoundError: +// return NewError(http.StatusNotFound, e.Error()) +// case *ValidationError: +// return NewError(http.StatusBadRequest, e.Error()) +// default: +// return NewError(http.StatusInternalServerError, "An unexpected error has occurred") +// } +// } +var HandleTypedError = func(err error) StatusError { + return NewError(http.StatusInternalServerError, err.Error()) +} + // WriteErr writes an error response with the given context, using the // configured error type and with the given status code and message. It is // marshaled using the API's content negotiation methods. diff --git a/huma.go b/huma.go index 7cd89e9d..41528fbe 100644 --- a/huma.go +++ b/huma.go @@ -1341,7 +1341,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) status = se.GetStatus() err = se } else { - err = NewError(http.StatusInternalServerError, err.Error()) + err = HandleTypedError(err) } ct, _ := api.Negotiate(ctx.Header("Accept")) diff --git a/huma_test.go b/huma_test.go index f61075f7..4f94255e 100644 --- a/huma_test.go +++ b/huma_test.go @@ -1940,6 +1940,45 @@ func TestCustomError(t *testing.T) { assert.Equal(t, `{"$schema":"http://localhost/schemas/MyError.json","message":"not found","details":["some-other-error"]}`+"\n", resp.Body.String()) } +type NotFoundError struct { + Resource string +} + +func (e *NotFoundError) Error() string { + return fmt.Sprintf("Resource '%s' not found", e.Resource) +} + +func TestTypedError(t *testing.T) { + orig := huma.HandleTypedError + defer func() { + huma.HandleTypedError = orig + }() + + huma.HandleTypedError = func(err error) huma.StatusError { + switch e := err.(type) { + case *NotFoundError: + return huma.NewError(http.StatusNotFound, e.Error()) + default: + return huma.NewError(http.StatusInternalServerError, "An unexpected error has occurred") + } + } + + _, api := humatest.New(t, huma.DefaultConfig("Test API", "1.0.0")) + + huma.Register(api, huma.Operation{ + OperationID: "get-error", + Method: http.MethodGet, + Path: "/error", + }, func(ctx context.Context, i *struct{}) (*struct{}, error) { + return nil, &NotFoundError{ + Resource: "foo", + } + }) + + resp := api.Get("/error", "Host: localhost") + assert.Equal(t, `{"$schema":"http://localhost/schemas/ErrorModel.json","title":"Not Found","status":404,"detail":"Resource 'foo' not found"}`+"\n", resp.Body.String()) +} + type NestedResolversStruct struct { Field2 string `json:"field2"` }