Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
148 changes: 96 additions & 52 deletions nexus/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
package nexus

import (
"encoding/json"
"errors"
"fmt"
"mime"
"net/url"
Expand All @@ -26,88 +24,105 @@ const (

const StatusUpstreamTimeout = 520

// A Failure represents failed handler invocations as well as `failed` or `canceled` operation results. Failures
// shouldn't typically be constructed directly. The SDK APIs take a [FailureConverter] instance that can translate
// language errors to and from [Failure] instances.
type Failure struct {
// A simple text message.
Message string `json:"message"`
// A key-value mapping for additional context. Useful for decoding the 'details' field, if needed.
Metadata map[string]string `json:"metadata,omitempty"`
// Additional JSON serializable structured data.
Details json.RawMessage `json:"details,omitempty"`
}

// An error that directly represents a wire representation of [Failure].
// The SDK will convert to this error by default unless the [FailureConverter] instance is customized.
type FailureError struct {
// The underlying Failure object this error represents.
Failure Failure
}

// Error implements the error interface.
func (e *FailureError) Error() string {
return e.Failure.Message
}

// OperationError represents "failed" and "canceled" operation results.
type OperationError struct {
// Error message.
Message string
// Stack trace which may be set if this error was generated by a language that supports it.
StackTrace string
// State of the operation. Only [OperationStateFailed] and [OperationStateCanceled] are valid.
State OperationState
// The underlying cause for this error.
Cause error
// Set if this error is constructed from a failure object.
OriginalFailure *Failure
Copy link
Contributor

Choose a reason for hiding this comment

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

I thought we didn't need this anymore since since we are using application failures?

Copy link
Contributor

Choose a reason for hiding this comment

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

Still required for the server to rehydrate the original failure.

}

// NewOperationFailedError is shorthand for constructing an [OperationError] with state set to
// [OperationStateFailed] and the given error message as the cause.
// [OperationStateFailed] and the given error message.
func NewOperationFailedError(message string) *OperationError {
return &OperationError{
State: OperationStateFailed,
Cause: errors.New(message),
State: OperationStateFailed,
Message: message,
// Also setting Cause as a temporary workaround for compatibility with older servers.
Cause: &FailureError{
Failure: Failure{
Message: message,
},
},
}
}

// OperationFailedErrorf creates an [OperationError] with state set to [OperationStateFailed], using [fmt.Errorf] to
// construct the cause.
// OperationFailedErrorf creates an [OperationError] with state set to [OperationStateFailed], using [fmt.Sprintf] to
// construct the message.
func OperationFailedErrorf(format string, args ...any) *OperationError {
Copy link
Contributor

Choose a reason for hiding this comment

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

This breaks error chaining no? if args was an error this used to preserve the error chain and now no longer does.

Copy link
Contributor

Choose a reason for hiding this comment

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

Same goes for HandlerErrorf

Copy link
Contributor

Choose a reason for hiding this comment

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

That was accidentally committed. I meant to revert back to the old implementations. Looks like I missed a couple.

return &OperationError{
State: OperationStateFailed,
Cause: fmt.Errorf(format, args...),
State: OperationStateFailed,
Message: fmt.Sprintf(format, args...),
// Also setting Cause as a temporary workaround for compatibility with older servers.
Cause: &FailureError{
Failure: Failure{
Message: fmt.Sprintf(format, args...),
},
},
}
}

// NewOperationCanceledError is shorthand for constructing an [OperationError] with state set to
// [OperationStateCanceled] and the given error message as the cause.
// [OperationStateCanceled] and the given error message.
func NewOperationCanceledError(message string) *OperationError {
return &OperationError{
State: OperationStateCanceled,
Cause: errors.New(message),
State: OperationStateCanceled,
Message: message,
// Also setting Cause as a temporary workaround for compatibility with older servers.
Cause: &FailureError{
Failure: Failure{
Message: message,
},
},
}
}

// OperationCanceledErrorf creates an [OperationError] with state set to [OperationStateCanceled], using [fmt.Errorf] to
// construct the cause.
// OperationCanceledErrorf creates an [OperationError] with state set to [OperationStateCanceled], using [fmt.Sprintf] to
// construct the message.
func OperationCanceledErrorf(format string, args ...any) *OperationError {
return &OperationError{
State: OperationStateCanceled,
Cause: fmt.Errorf(format, args...),
State: OperationStateCanceled,
Message: fmt.Sprintf(format, args...),
// Also setting Cause as a temporary workaround for compatibility with older servers.
Cause: &FailureError{
Failure: Failure{
Message: fmt.Sprintf(format, args...),
},
},
}
}

// OperationErrorf creates an [OperationError] with the given state, using [fmt.Errorf] to construct the cause.
// OperationErrorf creates an [OperationError] with the given state, using [fmt.Sprintf] to construct the message.
func OperationErrorf(state OperationState, format string, args ...any) *OperationError {
return &OperationError{
State: state,
Cause: fmt.Errorf(format, args...),
State: state,
Message: fmt.Sprintf(format, args...),
// Also setting Cause as a temporary workaround for compatibility with older servers.
Cause: &FailureError{
Failure: Failure{
Message: fmt.Sprintf(format, args...),
},
},
}
}

// Error implements the error interface.
func (e *OperationError) Error() string {
if e.Cause == nil {
return fmt.Sprintf("operation %s", e.State)
message := fmt.Sprintf("operation %s", e.State)
if len(e.Message) > 0 {
message += ": " + e.Message
} else if e.Cause != nil {
// Only append the cause if message is unset for compatibility with older SDKs which did not have a
// Message attribute.
message += ": " + e.Cause.Error()
}
return fmt.Sprintf("operation %s: %s", e.State, e.Cause.Error())
return message
}

// Unwrap returns the cause for use with utilities in the errors package.
Expand Down Expand Up @@ -176,18 +191,42 @@ const (
type HandlerError struct {
// Error Type. Defaults to HandlerErrorTypeInternal.
Type HandlerErrorType
// Error message.
Message string
// Stack trace which may be set if this error was generated by a language that supports it.
StackTrace string
// The underlying cause for this error.
Cause error
// RetryBehavior of this error. If not specified, retry behavior is determined from the error type.
RetryBehavior HandlerErrorRetryBehavior
// Set if this error is constructed from a failure object.
OriginalFailure *Failure
}

// HandlerErrorf creates a [HandlerError] with the given type, using [fmt.Errorf] to construct the cause.
// HandlerErrorf creates a [HandlerError] with the given type, using [fmt.Sprintf] to construct the message.
func HandlerErrorf(typ HandlerErrorType, format string, args ...any) *HandlerError {
return &HandlerError{
Type: typ,
Cause: fmt.Errorf(format, args...),
Type: typ,
Message: fmt.Sprintf(format, args...),
// Also setting Cause as a temporary workaround for compatibility with older servers.
Cause: &FailureError{
Failure: Failure{
Message: fmt.Sprintf(format, args...),
},
},
}
}

func (e *HandlerError) retryBehaviorAsOptionalBool() *bool {
switch e.RetryBehavior {
case HandlerErrorRetryBehaviorRetryable:
ret := true
return &ret
case HandlerErrorRetryBehaviorNonRetryable:
ret := false
return &ret
}
return nil
}

// Retryable returns a boolean indicating whether or not this error is retryable based on the error's RetryBehavior and
Expand Down Expand Up @@ -224,10 +263,15 @@ func (e *HandlerError) Error() string {
if len(typ) == 0 {
typ = HandlerErrorTypeInternal
}
if e.Cause == nil {
return fmt.Sprintf("handler error (%s)", typ)
message := fmt.Sprintf("handler error (%s)", typ)
if len(e.Message) > 0 {
message += ": " + e.Message
} else if e.Cause != nil {
// Only append the cause if message is unset for compatibility with older SDKs which did not have a
// Message attribute.
message += ": " + e.Cause.Error()
}
return fmt.Sprintf("handler error (%s): %s", typ, e.Cause.Error())
return message
}

// Unwrap returns the cause for use with utilities in the errors package.
Expand Down
10 changes: 7 additions & 3 deletions nexus/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,19 @@ func TestFailure_JSONMarshalling(t *testing.T) {

type testcase struct {
message string
stackTrace string
details any
metadata map[string]string
serialized string
}
cases := []testcase{
{
message: "simple",
details: "details",
message: "simple",
stackTrace: "stack",
details: "details",
serialized: `{
"message": "simple",
"stackTrace": "stack",
"details": "details"
}`,
},
Expand Down Expand Up @@ -55,7 +58,7 @@ func TestFailure_JSONMarshalling(t *testing.T) {
t.Run(tc.message, func(t *testing.T) {
serializedDetails, err := json.MarshalIndent(tc.details, "", "\t")
require.NoError(t, err)
source, err := json.MarshalIndent(Failure{tc.message, tc.metadata, serializedDetails}, "", "\t")
source, err := json.MarshalIndent(Failure{tc.message, tc.stackTrace, tc.metadata, serializedDetails, nil}, "", "\t")
require.NoError(t, err)
require.Equal(t, tc.serialized, string(source))

Expand All @@ -64,6 +67,7 @@ func TestFailure_JSONMarshalling(t *testing.T) {
require.NoError(t, err)

require.Equal(t, tc.message, failure.Message)
require.Equal(t, tc.stackTrace, failure.StackTrace)
require.Equal(t, tc.metadata, failure.Metadata)

detailsPointer := reflect.New(reflect.TypeOf(tc.details)).Interface()
Expand Down
Loading