From 02e2b3f5ce4647b8421249755d091c2fc1f4e983 Mon Sep 17 00:00:00 2001 From: Adrien Bustany Date: Wed, 2 Dec 2015 10:20:04 +0100 Subject: [PATCH 1/2] Add awserr.BatchedErrors type to describe batch errors --- aws/awserr/error.go | 17 +++++++++++++++ aws/awserr/types.go | 51 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/aws/awserr/error.go b/aws/awserr/error.go index a52743bef1c..576f1f7220b 100644 --- a/aws/awserr/error.go +++ b/aws/awserr/error.go @@ -103,3 +103,20 @@ type RequestFailure interface { func NewRequestFailure(err Error, statusCode int, reqID string) RequestFailure { return newRequestError(err, statusCode, reqID) } + +// A BatchedErrors is an interface to extract the list of errors that caused a +// batch call to fail. +type BatchedErrors interface { + Error + + // The status code of the HTTP response. + StatusCode() int + + // Errors returns the list of errors that caused the call to fail. + Errors() []Error +} + +// NewBatchedErrors returns a new batched errors wrapper for the given errors. +func NewBatchedErrors(code string, statusCode int, errors []Error) BatchedErrors { + return newBatchedErrors(code, statusCode, errors) +} diff --git a/aws/awserr/types.go b/aws/awserr/types.go index 003a6e8067e..6902c088433 100644 --- a/aws/awserr/types.go +++ b/aws/awserr/types.go @@ -1,6 +1,9 @@ package awserr -import "fmt" +import ( + "bytes" + "fmt" +) // SprintError returns a string of the formatted error code. // @@ -133,3 +136,49 @@ func (r requestError) StatusCode() int { func (r requestError) RequestID() string { return r.requestID } + +type batchedErrors struct { + code string + statusCode int + errors []Error +} + +func newBatchedErrors(code string, statusCode int, errors []Error) *batchedErrors { + return &batchedErrors{ + code: code, + statusCode: statusCode, + errors: errors, + } +} + +func (e batchedErrors) Error() string { + extra := fmt.Sprintf("status code: %d", e.statusCode) + return SprintError(e.Code(), e.Message(), extra, e.OrigErr()) +} + +func (e batchedErrors) Code() string { + return e.code +} + +func (e batchedErrors) Message() string { + message := bytes.NewBuffer([]byte{}) + + for _, err := range e.errors { + message.WriteString(err.Message()) + message.WriteByte('\n') + } + + return message.String() +} + +func (e batchedErrors) OrigErr() error { + return nil +} + +func (e batchedErrors) StatusCode() int { + return e.statusCode +} + +func (e batchedErrors) Errors() []Error { + return e.errors +} From c3b74ceacc3dbb2450940ea2800f16afb950a422 Mon Sep 17 00:00:00 2001 From: Adrien Bustany Date: Mon, 16 Nov 2015 14:54:49 +0100 Subject: [PATCH 2/2] Use custom unmarshalError handler for Route53 Certains Route53 calls like ChangeResourceRecordSets will return specific error types, so we need a custom handler to take those into account. --- service/route53/service.go | 4 +- service/route53/unmarshal_error.go | 81 ++++++++++++++++++++ service/route53/unmarshal_error_test.go | 98 +++++++++++++++++++++++++ 3 files changed, 182 insertions(+), 1 deletion(-) create mode 100644 service/route53/unmarshal_error.go create mode 100644 service/route53/unmarshal_error_test.go diff --git a/service/route53/service.go b/service/route53/service.go index 7711d53d17b..3e6c74a5aba 100644 --- a/service/route53/service.go +++ b/service/route53/service.go @@ -62,7 +62,9 @@ func newClient(cfg aws.Config, handlers request.Handlers, endpoint, signingRegio svc.Handlers.Build.PushBack(restxml.Build) svc.Handlers.Unmarshal.PushBack(restxml.Unmarshal) svc.Handlers.UnmarshalMeta.PushBack(restxml.UnmarshalMeta) - svc.Handlers.UnmarshalError.PushBack(restxml.UnmarshalError) + + // Route53 uses a custom error parser + svc.Handlers.UnmarshalError.PushBack(unmarshalError) // Run custom client initialization if present if initClient != nil { diff --git a/service/route53/unmarshal_error.go b/service/route53/unmarshal_error.go new file mode 100644 index 00000000000..c1dd8ff8dad --- /dev/null +++ b/service/route53/unmarshal_error.go @@ -0,0 +1,81 @@ +package route53 + +import ( + "bytes" + "encoding/xml" + "io/ioutil" + + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/private/protocol/restxml" +) + +type baseXMLErrorResponse struct { + XMLName xml.Name +} + +type standardXMLErrorResponse struct { + XMLName xml.Name `xml:"ErrorResponse"` + Code string `xml:"Error>Code"` + Message string `xml:"Error>Message"` + RequestID string `xml:"RequestId"` +} + +type invalidChangeBatchXMLErrorResponse struct { + XMLName xml.Name `xml:"InvalidChangeBatch"` + Messages []string `xml:"Messages>Message"` +} + +func unmarshalError(r *request.Request) { + switch r.Operation.Name { + case opChangeResourceRecordSets: + unmarshalChangeResourceRecordSetsError(r) + default: + restxml.UnmarshalError(r) + } +} + +func unmarshalChangeResourceRecordSetsError(r *request.Request) { + defer r.HTTPResponse.Body.Close() + + responseBody, err := ioutil.ReadAll(r.HTTPResponse.Body) + + if err != nil { + r.Error = awserr.New("SerializationError", "failed to read Route53 XML error response", err) + return + } + + baseError := &baseXMLErrorResponse{} + + if err := xml.Unmarshal(responseBody, baseError); err != nil { + r.Error = awserr.New("SerializationError", "failed to decode Route53 XML error response", err) + return + } + + switch baseError.XMLName.Local { + case "InvalidChangeBatch": + unmarshalInvalidChangeBatchError(r, responseBody) + default: + r.HTTPResponse.Body = ioutil.NopCloser(bytes.NewReader(responseBody)) + restxml.UnmarshalError(r) + } +} + +func unmarshalInvalidChangeBatchError(r *request.Request, requestBody []byte) { + resp := &invalidChangeBatchXMLErrorResponse{} + err := xml.Unmarshal(requestBody, resp) + + if err != nil { + r.Error = awserr.New("SerializationError", "failed to decode query XML error response", err) + return + } + + const errorCode = "InvalidChangeBatch" + errors := []awserr.Error{} + + for _, msg := range resp.Messages { + errors = append(errors, awserr.New(errorCode, msg, nil)) + } + + r.Error = awserr.NewBatchedErrors(errorCode, r.HTTPResponse.StatusCode, errors) +} diff --git a/service/route53/unmarshal_error_test.go b/service/route53/unmarshal_error_test.go new file mode 100644 index 00000000000..0952f7ab38a --- /dev/null +++ b/service/route53/unmarshal_error_test.go @@ -0,0 +1,98 @@ +package route53_test + +import ( + "bytes" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/awstesting/unit" + "github.com/aws/aws-sdk-go/service/route53" +) + +func makeClientWithResponse(response string) *route53.Route53 { + r := route53.New(unit.Session) + r.Handlers.Send.Clear() + r.Handlers.Send.PushBack(func(r *request.Request) { + body := ioutil.NopCloser(bytes.NewReader([]byte(response))) + r.HTTPResponse = &http.Response{ + ContentLength: int64(len(response)), + StatusCode: 400, + Status: "Bad Request", + Body: body, + } + }) + + return r +} + +func TestUnmarshalStandardError(t *testing.T) { + const errorResponse = ` + + + InvalidDomainName + The domain name is invalid + + 12345 + +` + + r := makeClientWithResponse(errorResponse) + + _, err := r.CreateHostedZone(&route53.CreateHostedZoneInput{ + CallerReference: aws.String("test"), + Name: aws.String("test_zone"), + }) + + assert.Error(t, err) + assert.Equal(t, "InvalidDomainName", err.(awserr.Error).Code()) + assert.Equal(t, "The domain name is invalid", err.(awserr.Error).Message()) +} + +func TestUnmarshalInvalidChangeBatch(t *testing.T) { + const errorMessage = ` +Tried to create resource record set duplicate.example.com. type A, +but it already exists +` + const errorResponse = ` + + + ` + errorMessage + ` + + +` + + r := makeClientWithResponse(errorResponse) + + req := &route53.ChangeResourceRecordSetsInput{ + HostedZoneId: aws.String("zoneId"), + ChangeBatch: &route53.ChangeBatch{ + Changes: []*route53.Change{ + &route53.Change{ + Action: aws.String("CREATE"), + ResourceRecordSet: &route53.ResourceRecordSet{ + Name: aws.String("domain"), + Type: aws.String("CNAME"), + TTL: aws.Int64(120), + ResourceRecords: []*route53.ResourceRecord{ + { + Value: aws.String("cname"), + }, + }, + }, + }, + }, + }, + } + + _, err := r.ChangeResourceRecordSets(req) + + assert.Error(t, err) + assert.Equal(t, "InvalidChangeBatch", err.(awserr.BatchedErrors).Code()) + assert.Equal(t, errorMessage+"\n", err.(awserr.BatchedErrors).Message()) +}