Skip to content

Commit

Permalink
feature: Add errors.ConjureErrorDecoder interface & instantiable regi…
Browse files Browse the repository at this point in the history
…stry type for customizing error deserialization (#724)
  • Loading branch information
bmoylan authored Nov 27, 2024
1 parent fa88981 commit 0e3ae59
Show file tree
Hide file tree
Showing 6 changed files with 101 additions and 22 deletions.
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()

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
}

0 comments on commit 0e3ae59

Please sign in to comment.