How to Satisfy the smithy.APIError Interface in Go #2858
-
Pre-Migration Checklist
Go Version UsedGo 1.23 Describe the Migration IssueSeeking the upgrade path for A simple, clear scenario is a function which creates a CloudWatch log group as needed. During certain well-understood race conditions the log group very well may exist by the time the code calls In Go SDK v1 this was easy: Code ComparisonFunction snippet following the migration guide for error unwrapping and taking distinct action for a particular error code: func (f FooStruct) MyCreateLogGrp(logGroupName string) (err error) {
_, err = f.client.CreateLogGroup(context.TODO(), &cloudwatchlogs.CreateLogGroupInput{
LogGroupName: aws.String(logGroupName),
})
if err != nil {
var s smithy.APIError
if errors.As(err, &s) {
var raee *types.ResourceAlreadyExistsException
if s.ErrorCode() == raee.ErrorCode() {
log.Info("swallowing error")
} else {
log.Error("something happened")
}
} else {
log.Error("did not satisfy smithy.APIError")
}
}
return
} also tried the simpler case to the CWL type directly, with effectively the same results: func (f FooStruct) MyCreateLogGrp(logGroupName string) (err error) {
_, err = f.client.CreateLogGroup(context.TODO(), &cloudwatchlogs.CreateLogGroupInput{
LogGroupName: aws.String(logGroupName),
})
if err != nil {
var raee *types.ResourceAlreadyExistsException
if errors.As(err, raee) {
log.Info("swallowing error")
} else {
log.Error("did not satisfy types.ResourceAlreadyExistsException")
}
}
return
} V1 test code which has worked fine: func TestMyCreateLogGrp(t *testing.T) {
mockController := gomock.NewController(t)
defer mockController.Finish()
mockCloudWatchLogs := mockcwl.NewMockCloudWatchLogsAPI(mockController)
mockCloudWatchLogs.EXPECT().CreateLogGroup(context.TODO(), gomock.Any()).Return(
&cloudwatchlogs.CreateLogGroupOutput{},
awserr.New(cloudwatchlogs.ErrCodeResourceAlreadyExistsException, "Log group already exists", nil),
)
...
} V2 test code returning a custom struct which ought to satisfy Smithy-Go, but unfortunately not working: type mockApiError struct {
ErrorCode string
ErrorMsg string
Fault smithy.ErrorFault
}
func TestMyCreateLogGrp(t *testing.T) {
mockController := gomock.NewController(t)
defer mockController.Finish()
mockCloudWatchLogs := mockcwlv2.NewMockCloudWatchLogsAPI(mockController)
var raee types.ResourceAlreadyExistsException
mockCloudWatchLogs.EXPECT().CreateLogGroup(context.TODO(), gomock.Any()).Return(
&cloudwatchlogs.CreateLogGroupOutput{},
mockApiError{
ErrorCode: raee.ErrorCode(),
ErrorMsg: raee.ErrorMessage(),
Fault: raee.ErrorFault(),
},
)
...
} also tried logging an SDK v2 error in an isolated environment, and copying-pasting its error string to func TestMyCreateLogGrp(t *testing.T) {
mockController := gomock.NewController(t)
defer mockController.Finish()
mockCloudWatchLogs := mockcwlv2.NewMockCloudWatchLogsAPI(mockController)
mockCloudWatchLogs.EXPECT().CreateLogGroup(context.TODO(), gomock.Any()).Return(
&cloudwatchlogs.CreateLogGroupOutput{},
errors.New("operation error CloudWatch Logs: CreateLogGroup, https response error StatusCode: 400, RequestID: b978xxxx-xxxx-xxxx-xxxx-xxxx1ef4cf89, ResourceAlreadyExistsException: The specified log group already exists"),
)
...
} Observed Differences/ErrorsIn both of the above V2 variations, the code yields the Additional ContextNo response |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 1 reply
-
This doesn't actually satisfy Double-check that you're satisfying the interface w/ var _ smithy.APIError = (*mockApiError)(nil) You want something like type mockError struct {
code, msg string
fault smithy.ErrorFault
}
func(m *mockError) ErrorCode() string { return m.code }
func(m *mockError) ErrorMessage() string { return m.msg }
func(m *mockError) ErrorFault() smithy.ErrorFault { return m.fault }
func(m *mockError) Error() string { return "..." }
var _ smithy.APIError = (*mockError)(nil) You can also just use the concrete |
Beta Was this translation helpful? Give feedback.
-
Thank you for the reply. I had not made any effort to satisfy the interface, only matched the struct fields. So the path forward seems to be to satisfy the interface. When I use var raee types.ResourceAlreadyExistsException
mockCloudWatchLogs.EXPECT().CreateLogGroup(context.TODO(), gomock.Any()).Return(
&cloudwatchlogs.CreateLogGroupOutput{},
smithy.GenericAPIError{
Code: raee.ErrorCode(),
Message: raee.ErrorMessage(),
Fault: raee.ErrorFault(),
},
) I get this error:
Am I using it incorrectly? |
Beta Was this translation helpful? Give feedback.
-
Correct, this is the case across the board in Go. "Is this an X" checks at runtime are idiomatically done via checking if something satisfies an interface. (that's what errors.As is doing under the hood) I think it needs to be |
Beta Was this translation helpful? Give feedback.
-
I'd contend there is still a gap in the SDK v2 page on error handling, which is the "more info" link from the migration guide. For instance, it states this:
This is unclear whether it's the AWS service endpoint's API response which implements it or whether it's the Go SDK that is wrapping the underlying error to ensure the end result satisfies it. In fact, I would have guessed the latter based on this statement in the previous section:
Almost reads like I can still use This would have made a lot more sense to me if the migration guide had included a quick example of v1 I see this issue has been converted to a Discussion though. Do I need to suggest this more directly in a new issue, or can it be spun off from here? |
Beta Was this translation helpful? Give feedback.
This doesn't actually satisfy
smithy.APIError
though - you don't have the corresponding methods on that struct, unless your sample just doesn't show them (don't see how it could, though - you have an ErrorCode field, so you can't have an ErrorCode() method too)Double-check that you're satisfying the interface w/
You want something like