diff --git a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsGoDependency.java b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsGoDependency.java index 3a5e94001e5..41b8d7a04d4 100644 --- a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsGoDependency.java +++ b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/AwsGoDependency.java @@ -81,6 +81,6 @@ protected static GoDependency module( } private static final class Versions { - private static final String AWS_SDK = "v0.0.0-20201001231852-1fc1ab173989"; + private static final String AWS_SDK = "v0.0.0-20201002231452-4f578e93925d"; } } diff --git a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/AwsCustomGoDependency.java b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/AwsCustomGoDependency.java index cc354182804..6dbe43e3316 100644 --- a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/AwsCustomGoDependency.java +++ b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/AwsCustomGoDependency.java @@ -37,6 +37,8 @@ public final class AwsCustomGoDependency extends AwsGoDependency { "service/kinesis/internal/customizations", "kinesiscust"); public static final GoDependency MACHINE_LEARNING_CUSTOMIZATION = aws( "service/machinelearning/internal/customizations", "mlcust"); + public static final GoDependency ROUTE53_CUSTOMIZATION = aws( + "service/route53/internal/customizations", "route53cust"); private AwsCustomGoDependency() { super(); diff --git a/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/Route53ErrorCustomizations.java b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/Route53ErrorCustomizations.java new file mode 100644 index 00000000000..7f964c3f98e --- /dev/null +++ b/codegen/smithy-aws-go-codegen/src/main/java/software/amazon/smithy/aws/go/codegen/customization/Route53ErrorCustomizations.java @@ -0,0 +1,51 @@ +package software.amazon.smithy.aws.go.codegen.customization; + +import java.util.List; +import software.amazon.smithy.aws.traits.ServiceTrait; +import software.amazon.smithy.go.codegen.SymbolUtils; +import software.amazon.smithy.go.codegen.integration.GoIntegration; +import software.amazon.smithy.go.codegen.integration.MiddlewareRegistrar; +import software.amazon.smithy.go.codegen.integration.RuntimeClientPlugin; +import software.amazon.smithy.model.Model; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ServiceShape; +import software.amazon.smithy.utils.ListUtils; + +public class Route53ErrorCustomizations implements GoIntegration { + private static String ADD_ERROR_HANDLER_INTERNAL = "HandleCustomErrorDeserialization"; + + @Override + public byte getOrder() { + // The associated customization ordering is relative to operation deserializers + // and thus the integration should be added at the end. + return 127; + } + + @Override + public List getClientPlugins() { + return ListUtils.of( + RuntimeClientPlugin.builder() + .operationPredicate(Route53ErrorCustomizations::supportsCustomError) + .registerMiddleware(MiddlewareRegistrar.builder() + .resolvedFunction(SymbolUtils.createValueSymbolBuilder(ADD_ERROR_HANDLER_INTERNAL, + AwsCustomGoDependency.ROUTE53_CUSTOMIZATION).build()) + .build()) + .build() + ); + } + + // returns true if the operation supports custom route53 error response + private static boolean supportsCustomError(Model model, ServiceShape service, OperationShape operation){ + if (!isRoute53Service(model, service)) { + return false; + } + + return operation.getId().getName().equalsIgnoreCase("ChangeResourceRecordSets"); + } + + // returns true if service is route53 + private static boolean isRoute53Service(Model model, ServiceShape service) { + String serviceId= service.expectTrait(ServiceTrait.class).getSdkId(); + return serviceId.equalsIgnoreCase("Route 53"); + } +} diff --git a/codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration b/codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration index 96901634328..2d1c940d64f 100644 --- a/codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration +++ b/codegen/smithy-aws-go-codegen/src/main/resources/META-INF/services/software.amazon.smithy.go.codegen.integration.GoIntegration @@ -22,3 +22,4 @@ software.amazon.smithy.aws.go.codegen.customization.MachineLearningCustomization software.amazon.smithy.aws.go.codegen.customization.S3AcceptEncodingGzip software.amazon.smithy.aws.go.codegen.customization.KinesisCustomizations software.amazon.smithy.aws.go.codegen.customization.S3ErrorWith200Status +software.amazon.smithy.aws.go.codegen.customization.Route53ErrorCustomizations diff --git a/service/route53/api_op_ChangeResourceRecordSets.go b/service/route53/api_op_ChangeResourceRecordSets.go index a8d18e15b7e..4256d291683 100644 --- a/service/route53/api_op_ChangeResourceRecordSets.go +++ b/service/route53/api_op_ChangeResourceRecordSets.go @@ -7,6 +7,7 @@ import ( awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware" "github.com/aws/aws-sdk-go-v2/aws/retry" "github.com/aws/aws-sdk-go-v2/aws/signer/v4" + route53cust "github.com/aws/aws-sdk-go-v2/service/route53/internal/customizations" "github.com/aws/aws-sdk-go-v2/service/route53/types" smithy "github.com/awslabs/smithy-go" "github.com/awslabs/smithy-go/middleware" @@ -95,6 +96,7 @@ func (c *Client) ChangeResourceRecordSets(ctx context.Context, params *ChangeRes stack.Initialize.Add(newServiceMetadataMiddleware_opChangeResourceRecordSets(options.Region), middleware.Before) addRequestIDRetrieverMiddleware(stack) addResponseErrorMiddleware(stack) + route53cust.HandleCustomErrorDeserialization(stack) for _, fn := range options.APIOptions { if err := fn(stack); err != nil { diff --git a/service/route53/go.mod b/service/route53/go.mod index 10c6dd76db7..bb6f7d60072 100644 --- a/service/route53/go.mod +++ b/service/route53/go.mod @@ -3,7 +3,7 @@ module github.com/aws/aws-sdk-go-v2/service/route53 go 1.15 require ( - github.com/aws/aws-sdk-go-v2 v0.0.0-20201001231852-1fc1ab173989 + github.com/aws/aws-sdk-go-v2 v0.0.0-20201005175632-36f8dc6bcc52 github.com/awslabs/smithy-go v0.1.1 ) diff --git a/service/route53/internal/customizations/custom_error_deser.go b/service/route53/internal/customizations/custom_error_deser.go new file mode 100644 index 00000000000..46a5cf1b8f8 --- /dev/null +++ b/service/route53/internal/customizations/custom_error_deser.go @@ -0,0 +1,93 @@ +package customizations + +import ( + "bytes" + "context" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "strings" + + "github.com/awslabs/smithy-go" + "github.com/awslabs/smithy-go/middleware" + "github.com/awslabs/smithy-go/ptr" + smithyhttp "github.com/awslabs/smithy-go/transport/http" + smithyxml "github.com/awslabs/smithy-go/xml" + + awsmiddle "github.com/aws/aws-sdk-go-v2/aws/middleware" + "github.com/aws/aws-sdk-go-v2/service/route53/types" +) + +// HandleCustomErrorDeserialization check if Route53 response is an error and needs +// custom error deserialization. +// +func HandleCustomErrorDeserialization(stack *middleware.Stack) { + stack.Deserialize.Insert(&processResponseMiddleware{}, "OperationDeserializer", middleware.After) +} + +// middleware to process raw response and look for error response with InvalidChangeBatch error tag +type processResponseMiddleware struct{} + +// ID returns the middleware ID. +func (*processResponseMiddleware) ID() string { return "Route53:ProcessResponseForCustomErrorResponse" } + +func (m *processResponseMiddleware) HandleDeserialize( + ctx context.Context, in middleware.DeserializeInput, next middleware.DeserializeHandler) ( + out middleware.DeserializeOutput, metadata middleware.Metadata, err error, +) { + out, metadata, err = next.HandleDeserialize(ctx, in) + if err != nil { + return out, metadata, err + } + + response, ok := out.RawResponse.(*smithyhttp.Response) + if !ok { + return out, metadata, &smithy.DeserializationError{Err: fmt.Errorf("unknown transport type %T", out.RawResponse)} + } + + // check if success response + if response.StatusCode >= 200 && response.StatusCode < 300 { + return + } + + var readBuff bytes.Buffer + body := io.TeeReader(response.Body, &readBuff) + + rootDecoder := xml.NewDecoder(body) + t, err := smithyxml.FetchRootElement(rootDecoder) + if err == io.EOF { + return out, metadata, nil + } + + // rewind response body + response.Body = ioutil.NopCloser(io.MultiReader(&readBuff, response.Body)) + + // if start tag is "InvalidChangeBatch", the error response needs custom unmarshaling. + if strings.EqualFold(t.Name.Local, "InvalidChangeBatch") { + return out, metadata, route53CustomErrorDeser(&metadata, response) + } + + return out, metadata, err +} + +// error type for invalidChangeBatchError +type invalidChangeBatchError struct { + Messages []string `xml:"Messages>Message"` + RequestID string `xml:"RequestId"` +} + +func route53CustomErrorDeser(metadata *middleware.Metadata, response *smithyhttp.Response) error { + err := invalidChangeBatchError{} + xml.NewDecoder(response.Body).Decode(&err) + + // set request id in metadata + if len(err.RequestID) != 0 { + awsmiddle.SetRequestIDMetadata(metadata, err.RequestID) + } + + return &types.InvalidChangeBatch{ + Message: ptr.String("ChangeBatch errors occurred"), + Messages: ptr.StringSlice(err.Messages), + } +} diff --git a/service/route53/internal/customizations/custom_error_deser_test.go b/service/route53/internal/customizations/custom_error_deser_test.go new file mode 100644 index 00000000000..3dcc173fe6b --- /dev/null +++ b/service/route53/internal/customizations/custom_error_deser_test.go @@ -0,0 +1,123 @@ +package customizations_test + +import ( + "context" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/route53" + "github.com/aws/aws-sdk-go-v2/service/route53/types" +) + +func TestCustomErrorDeserialization(t *testing.T) { + cases := map[string]struct { + responseStatus int + responseBody []byte + expectedError string + expectedRequestID string + expectedResponseID string + }{ + "invalidChangeBatchError": { + responseStatus: 500, + responseBody: []byte(` + + + Tried to create resource record set duplicate.example.com. type A, but it already exists + + b25f48e8-84fd-11e6-80d9-574e0c4664cb + `), + expectedError: "InvalidChangeBatch: ChangeBatch errors occurred", + expectedRequestID: "b25f48e8-84fd-11e6-80d9-574e0c4664cb", + }, + + "standardRestXMLError": { + responseStatus: 500, + responseBody: []byte(` + + + Sender + MalformedXML + 1 validation error detected: Value null at 'route53#ChangeSet' failed to satisfy constraint: Member must not be null + + b25f48e8-84fd-11e6-80d9-574e0c4664cb + + `), + expectedError: "1 validation error detected:", + expectedRequestID: "b25f48e8-84fd-11e6-80d9-574e0c4664cb", + }, + + "Success response": { + responseStatus: 200, + responseBody: []byte(` + + + mockComment + mockID + + `), + expectedResponseID: "mockID", + }, + } + + for name, c := range cases { + server := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(c.responseStatus) + w.Write(c.responseBody) + })) + defer server.Close() + + t.Run(name, func(t *testing.T) { + svc := route53.NewFromConfig(aws.Config{ + Region: "us-east-1", + EndpointResolver: aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) { + return aws.Endpoint{ + URL: server.URL, + SigningName: "route53", + }, nil + }), + Retryer: aws.NoOpRetryer{}, + }) + resp, err := svc.ChangeResourceRecordSets(context.Background(), &route53.ChangeResourceRecordSetsInput{ + ChangeBatch: &types.ChangeBatch{ + Changes: []*types.Change{}, + Comment: aws.String("mock"), + }, + HostedZoneId: aws.String("zone"), + }) + + if err == nil && len(c.expectedError) != 0 { + t.Fatalf("expected err, got none") + } + + if len(c.expectedError) != 0 { + if e, a := c.expectedError, err.Error(); !strings.Contains(a, e) { + t.Fatalf("expected error to be %s, got %s", e, a) + } + + var responseError interface { + ServiceRequestID() string + } + + if !errors.As(err, &responseError) { + t.Fatalf("expected error to be of type %T, was not", responseError) + } + + if e, a := c.expectedRequestID, responseError.ServiceRequestID(); !strings.EqualFold(e, a) { + t.Fatalf("expected request id to be %s, got %s", e, a) + } + } + + if len(c.expectedResponseID) != 0 { + if e, a := c.expectedResponseID, *resp.ChangeInfo.Id; !strings.EqualFold(e, a) { + t.Fatalf("expected response to have id %v, got %v", e, a) + } + } + + }) + } +} diff --git a/service/route53/internal/customizations/doc.go b/service/route53/internal/customizations/doc.go new file mode 100644 index 00000000000..46b9bcd4dfb --- /dev/null +++ b/service/route53/internal/customizations/doc.go @@ -0,0 +1,41 @@ +// Package customizations provides customizations for the Amazon Route53 API client. +// +// This package provides support for following customizations +// +// Process Response Middleware: used for custom error deserializing +// +// +// Process Response Middleware +// +// Route53 operation "ChangeResourceRecordSets" can have an error response returned in +// a slightly different format. This customization is only applicable to +// ChangeResourceRecordSets operation of Route53. +// +// Here's a sample error response: +// +// +// +// +// Tried to create resource record set duplicate.example.com. type A, but it already exists +// +// +// +// +// The processResponse middleware customizations enables SDK to check for an error +// response starting with "InvalidChangeBatch" tag prior to deserialization. +// +// As this check in error response needs to be performed earlier than response +// deserialization. Since the behavior of Deserialization is in +// reverse order to the other stack steps its easier to consider that "after" means +// "before". +// +// Middleware layering: +// +// HTTP Response -> process response error -> deserialize +// +// +// In case the returned error response has `InvalidChangeBatch` format, the error is +// deserialized and returned. The operation deserializer does not attempt to deserialize +// as an error is returned by the process response error middleware. +// +package customizations