Skip to content

Commit

Permalink
Merge pull request #792 from aws/route53-unmarshalError
Browse files Browse the repository at this point in the history
  • Loading branch information
skotambkar authored Oct 5, 2020
2 parents 5861529 + 2684125 commit a63fa81
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RuntimeClientPlugin> 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions service/route53/api_op_ChangeResourceRecordSets.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion service/route53/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)

Expand Down
93 changes: 93 additions & 0 deletions service/route53/internal/customizations/custom_error_deser.go
Original file line number Diff line number Diff line change
@@ -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),
}
}
123 changes: 123 additions & 0 deletions service/route53/internal/customizations/custom_error_deser_test.go
Original file line number Diff line number Diff line change
@@ -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(`<?xml version="1.0" encoding="UTF-8"?>
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<Messages>
<Message>Tried to create resource record set duplicate.example.com. type A, but it already exists</Message>
</Messages>
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
</InvalidChangeBatch>`),
expectedError: "InvalidChangeBatch: ChangeBatch errors occurred",
expectedRequestID: "b25f48e8-84fd-11e6-80d9-574e0c4664cb",
},

"standardRestXMLError": {
responseStatus: 500,
responseBody: []byte(`<?xml version="1.0"?>
<ErrorResponse xmlns="http://route53.amazonaws.com/doc/2016-09-07/">
<Error>
<Type>Sender</Type>
<Code>MalformedXML</Code>
<Message>1 validation error detected: Value null at 'route53#ChangeSet' failed to satisfy constraint: Member must not be null</Message>
</Error>
<RequestId>b25f48e8-84fd-11e6-80d9-574e0c4664cb</RequestId>
</ErrorResponse>
`),
expectedError: "1 validation error detected:",
expectedRequestID: "b25f48e8-84fd-11e6-80d9-574e0c4664cb",
},

"Success response": {
responseStatus: 200,
responseBody: []byte(`<?xml version="1.0" encoding="UTF-8"?>
<ChangeResourceRecordSetsResponse>
<ChangeInfo>
<Comment>mockComment</Comment>
<Id>mockID</Id>
</ChangeInfo>
</ChangeResourceRecordSetsResponse>`),
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)
}
}

})
}
}
41 changes: 41 additions & 0 deletions service/route53/internal/customizations/doc.go
Original file line number Diff line number Diff line change
@@ -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:
//
// <?xml version="1.0" encoding="UTF-8"?>
// <InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
// <Messages>
// <Message>Tried to create resource record set duplicate.example.com. type A, but it already exists</Message>
// </Messages>
// </InvalidChangeBatch>
//
//
// 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

0 comments on commit a63fa81

Please sign in to comment.