Skip to content

Commit

Permalink
Make huma play well with custom error types returned from handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
camcalaquian committed Jul 15, 2024
1 parent b8be41a commit 5ab1dea
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 1 deletion.
37 changes: 37 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Check warning on line 285 in error.go

View check run for this annotation

Codecov / codecov/patch

error.go#L284-L285

Added lines #L284 - L285 were not covered by tests
}

// 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.
Expand Down
2 changes: 1 addition & 1 deletion huma.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
39 changes: 39 additions & 0 deletions huma_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down

0 comments on commit 5ab1dea

Please sign in to comment.