Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add errors.ConjureErrorDecoder interface & instantiable registry type for customizing error deserialization #724

Merged
merged 6 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions changelog/@unreleased/pr-724.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type: feature
feature:
description: Add errors.ConjureErrorDecoder interface & instantiable registry type
for customizing error deserialization
links:
- https://github.com/palantir/conjure-go-runtime/pull/724
12 changes: 12 additions & 0 deletions conjure-go-client/httpclient/request_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"time"

"github.com/palantir/conjure-go-runtime/v2/conjure-go-contract/codecs"
"github.com/palantir/conjure-go-runtime/v2/conjure-go-contract/errors"
werror "github.com/palantir/witchcraft-go-error"
)

Expand Down Expand Up @@ -243,3 +244,14 @@ func WithRequestTimeout(timeout time.Duration) RequestParam {
return nil
})
}

func WithRequestConjureErrorDecoder(ced errors.ConjureErrorDecoder) RequestParam {
return requestParamFunc(func(b *requestBuilder) error {
b.errorDecoderMiddleware = errorDecoderMiddleware{
errorDecoder: restErrorDecoder{
conjureErrorDecoder: ced,
},
}
return nil
})
}
12 changes: 10 additions & 2 deletions conjure-go-client/httpclient/response_error_decoder_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ func (e errorDecoderMiddleware) RoundTrip(req *http.Request, next http.RoundTrip
// If the response has a Content-Type containing 'application/json', we attempt
// to unmarshal the error as a conjure error. See TestErrorDecoderMiddlewares for
// example error messages and parameters.
type restErrorDecoder struct{}
type restErrorDecoder struct {
conjureErrorDecoder errors.ConjureErrorDecoder
}

var _ ErrorDecoder = restErrorDecoder{}

Expand Down Expand Up @@ -102,7 +104,13 @@ func (d restErrorDecoder) DecodeError(resp *http.Response) error {
if isJSON := strings.Contains(resp.Header.Get("Content-Type"), codecs.JSON.ContentType()); !isJSON {
return werror.Error(resp.Status, wSafeParams, wUnsafeParams, werror.UnsafeParam("responseBody", string(body)))
}
conjureErr, jsonErr := errors.UnmarshalError(body)
var conjureErr errors.Error
var jsonErr error
if d.conjureErrorDecoder != nil {
conjureErr, jsonErr = errors.UnmarshalErrorWithDecoder(d.conjureErrorDecoder, body)
} else {
conjureErr, jsonErr = errors.UnmarshalError(body)
}
if jsonErr != nil {
return werror.Error(resp.Status, wSafeParams, wUnsafeParams, werror.UnsafeParam("responseBody", string(body)))
}
Expand Down
19 changes: 19 additions & 0 deletions conjure-go-contract/errors/conjure_error_decoder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright (c) 2024 Palantir Technologies. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package errors

type ConjureErrorDecoder interface {
DecodeConjureError(name string, body []byte) (Error, error)
}
50 changes: 44 additions & 6 deletions conjure-go-contract/errors/error_type_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,59 @@ package errors
import (
"fmt"
"reflect"

"github.com/palantir/conjure-go-runtime/v2/conjure-go-contract/codecs"
werror "github.com/palantir/witchcraft-go-error"
)

var registry = map[string]reflect.Type{}
var globalRegistry = NewReflectTypeConjureErrorDecoder()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice 👍


var errorInterfaceType = reflect.TypeOf((*Error)(nil)).Elem()

// RegisterErrorType registers an error name and its go type in a global registry.
// The type should be a struct type whose pointer implements Error.
// Panics if name is already registered or *type does not implement Error.
func RegisterErrorType(name string, typ reflect.Type) {
if existing, exists := registry[name]; exists {
panic(fmt.Sprintf("ErrorName %v already registered as %v", name, existing))
if err := globalRegistry.RegisterErrorType(name, typ); err != nil {
panic(err.Error())
}
}

// NewReflectTypeConjureErrorDecoder returns a new ConjureErrorDecoder that uses reflection to convert JSON errors to their go types.
func NewReflectTypeConjureErrorDecoder() *ReflectTypeConjureErrorDecoder {
return &ReflectTypeConjureErrorDecoder{registry: make(map[string]reflect.Type)}
}

// ReflectTypeConjureErrorDecoder is a ConjureErrorDecoder that uses reflection to convert JSON errors to their go types.
// It stores a mapping of serialized error name to the go type that should be used to unmarshal the error.
type ReflectTypeConjureErrorDecoder struct {
registry map[string]reflect.Type
}

func (d *ReflectTypeConjureErrorDecoder) RegisterErrorType(name string, typ reflect.Type) error {
if existing, exists := d.registry[name]; exists {
return fmt.Errorf("ErrorName %v already registered as %v", name, existing)
}
if ptr := reflect.PointerTo(typ); !ptr.Implements(errorInterfaceType) {
return fmt.Errorf("Error type %v does not implement errors.Error interface", ptr)
}
d.registry[name] = typ
return nil
}

func (d *ReflectTypeConjureErrorDecoder) DecodeConjureError(errorName string, body []byte) (Error, error) {
typ, ok := d.registry[errorName]
if !ok {
// Unrecognized error name, fall back to genericError
typ = reflect.TypeOf(genericError{})
}
instance := reflect.New(typ).Interface()
if err := codecs.JSON.Unmarshal(body, &instance); err != nil {
return nil, werror.Wrap(err, "failed to unmarshal body using registered type", werror.SafeParam("type", typ.String()))
}
if ptr := reflect.PtrTo(typ); !ptr.Implements(errorInterfaceType) {
panic(fmt.Sprintf("Error type %v does not implement errors.Error interface", ptr))
cerr, ok := instance.(Error)
if !ok {
return nil, werror.Error("unmarshaled type does not implement errors.Error interface", werror.SafeParam("type", typ.String()))
}
registry[name] = typ
return cerr, nil
}
24 changes: 10 additions & 14 deletions conjure-go-contract/errors/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@
package errors

import (
"reflect"

"github.com/palantir/conjure-go-runtime/v2/conjure-go-contract/codecs"
werror "github.com/palantir/witchcraft-go-error"
)
Expand All @@ -26,23 +24,21 @@ import (
// If the ErrorName is not recognized, a genericError is returned with all params marked unsafe.
// If we fail to unmarshal to a generic SerializableError or to the type specified by ErrorName, an error is returned.
func UnmarshalError(body []byte) (Error, error) {
return UnmarshalErrorWithDecoder(globalRegistry, body)
}

// UnmarshalErrorWithDecoder attempts to deserialize the message to a known implementation of Error
// using the provided ConjureErrorDecoder.
func UnmarshalErrorWithDecoder(ced ConjureErrorDecoder, body []byte) (Error, error) {
var name struct {
Name string `json:"errorName"`
}
if err := codecs.JSON.Unmarshal(body, &name); err != nil {
return nil, werror.Wrap(err, "failed to unmarshal body as conjure error")
}
typ, ok := registry[name.Name]
if !ok {
// Unrecognized error name, fall back to genericError
typ = reflect.TypeOf(genericError{})
cErr, err := ced.DecodeConjureError(name.Name, body)
if err != nil {
return nil, werror.Convert(err)
}

instance := reflect.New(typ).Interface()
if err := codecs.JSON.Unmarshal(body, &instance); err != nil {
return nil, werror.Wrap(err, "failed to unmarshal body using registered type", werror.SafeParam("type", typ.String()))
}

// Cast should never panic, as we've verified in RegisterErrorType
return instance.(Error), nil
return cErr, nil
}