question/bug: Is there a way to use custom errors for Goa's internal errors?
The example here shows how to return a custom error: https://github.com/goadesign/examples/blob/master/error/calc.go#L26
This is good but if one wants to customize the error type for a whole service, Goa still uses its own underlying ErrorResult type
which can not be customized at the moment or am I missing something?
Also the client uses ClientError: from goa/v3/http/client.go:
and a decode function in the client/encode_decode.go is generated like the blow extract
and any server error due to request decoding (ErrorResult) (e.g. missing field)
gets mapped into the default: case which returns a gohttp.ErrInvalidResponse. Is this correct?
We defined the errors cm#bad-request and cm#invalid-contract etc.
*Encode/Decode in Client:
func DecodeCreateContractResponse(decoder func(*http.Response) goahttp.Decoder, restoreBody bool) func(*http.Response) (any, error) {
return func(resp *http.Response) (any, error) {
// redacted .........
switch resp.StatusCode {
case http.StatusOK:
// redacted ...
case http.StatusBadRequest:
en := resp.Header.Get("goa-error")
switch en {
case "cm#bad-request":
var (
body CreateContractCmBadRequestResponseBody
err error
)
err = decoder(resp).Decode(&body)
if err != nil {
return nil, goahttp.ErrDecodingError("cm", "createContract", err)
}
err = ValidateCreateContractCmBadRequestResponseBody(&body)
if err != nil {
return nil, goahttp.ErrValidationError("cm", "createContract", err)
}
return nil, NewCreateContractCmBadRequest(&body)
case "cm#contract-invalid":
var (
body CreateContractCmContractInvalidResponseBody
err error
)
err = decoder(resp).Decode(&body)
if err != nil {
return nil, goahttp.ErrDecodingError("cm", "createContract", err)
}
err = ValidateCreateContractCmContractInvalidResponseBody(&body)
if err != nil {
return nil, goahttp.ErrValidationError("cm", "createContract", err)
}
return nil, NewCreateContractCmContractInvalid(&body)
default:
body, _ := io.ReadAll(resp.Body)
return nil, goahttp.ErrInvalidResponse("cm", "createContract", resp.StatusCode, string(body))
}
Hello,
- For errors that service endpoints return the Goa DSL makes it possible to specify custom error types (like in your example). If there's a need to default to a different type then one can write a custom DSL function that wraps the existing
Errorfunction and sets the type, something like:
func MyCustomError(name, desc string) {
Error(name, MyCustomErrorType, desc)
}
Where MyCustomErrorType identifies a field as the error name using ErrorName
The service can then use MyCustomError("foo", "bar") instead of Error("foo", MyCustomErrorType, "bar") which might help with consistency.
- For errors that Goa validations return error formatters make it possible to override how they get serialized. For example an error formatter could format all possible validation errors to reuse the same
MyCustomErrorType.
Client side things should "just work" for service endpoint errors (as per your code snippet above) assuming the error object is valid (passes the validations defined in the error type design). The generated server code sets the "goa-error" header which the client leverages to map to the proper type. However errors that use a custom formatter end up being mapped to a generic error type so validation errors will have a different data type on the client.
The Error Handling section of the docs covers the above.
Thanks for the answer!
We implemented exactly this approach with a formatter
https://gitlab.com/data-custodian/custodian/-/blob/feat/continue-with-testing/components/lib-common/pkg/response/error/goa/formatter.go?ref_type=heads#L46
and I realized that on the client side validation errors get mapped to some generic error which is unfortunate. The response is now, ** with the formatter above**, everywhere the same: for our custom errors and for goa validation errors, but when writting integration tests I can only catch our custom errors but as said the goa validation errors are a generic http.ClientError from goahttp.ErrInvalidResponse
Why is that so, why are these validation errors not properly mapped? Could that be added as a feature maybe, I think strong type safety helps a lot here. AFAIS this behavior is independent of the usage of a custom formatter.
Makes sense, the challenge here is that by definition Goa has no control on what the custom formatter produces and thus wouldn't know how to initialize a validation error response from the HTTP response body. To tackle this there would have to be a way to inject user code that would understand the custom format and could do this - this is probably the right long term approach but it might be tricky to do in a way that doesn't break the current contract (the formatter is given to the generated NewServer function and that would have to change so that one could inject the "unformatter" as well).
However it might be possible to emulate the above today by:
- Defining a "BadRequest" error for all methods and mapping that to the 400 status in HTTP responses
- Implementing a client side HTTP middleware that intercepts the formatted response and replaces the body with one that is compatible with Goa's ErrorResponse
This way the Goa generated code client side will unmarshal the body into a ServiceError
(note: I haven't tried the above but AFAIK it should work - happy to answer questions that might come up)
Thanks for the explanation that helps. Jeah it would be nice if NewServ function could take the an ErrorMarshaller interface and the client a ErrorUnmarshaller interface (?).
I guess changing this is a major version upgrade...
I might need to try this middleware approach because it makes testing much easier.