Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use custom unmarshalError handler for Route53 #438

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions aws/awserr/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
51 changes: 50 additions & 1 deletion aws/awserr/types.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package awserr

import "fmt"
import (
"bytes"
"fmt"
)

// SprintError returns a string of the formatted error code.
//
Expand Down Expand Up @@ -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
}
4 changes: 3 additions & 1 deletion service/route53/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
81 changes: 81 additions & 0 deletions service/route53/unmarshal_error.go
Original file line number Diff line number Diff line change
@@ -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)
}
98 changes: 98 additions & 0 deletions service/route53/unmarshal_error_test.go
Original file line number Diff line number Diff line change
@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<ErrorResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<Error>
<Code>InvalidDomainName</Code>
<Message>The domain name is invalid</Message>
</Error>
<RequestId>12345</RequestId>
</ErrorResponse>
`

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 = `<?xml version="1.0" encoding="UTF-8"?>
<InvalidChangeBatch xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
<Messages>
<Message>` + errorMessage + `</Message>
</Messages>
</InvalidChangeBatch>
`

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())
}