diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8a1927a39ca..5934cb21f81 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,5 +1,6 @@ ### SDK Features ### SDK Enhancements - +* `service/cloudwatch`: Add helper to send request payload as GZIP content encoding + * Adds a new helper, `WithGzipRequest` to the `cloudwatch` package. The helper will configure the payload to be sent as `content-encoding: gzip`. It is supported by operations like `PutMetricData`. See the service's API Reference documentation for other operations supported. ### SDK Bugs diff --git a/internal/encoding/gzip/handler.go b/internal/encoding/gzip/handler.go new file mode 100644 index 00000000000..ea0d4bc2477 --- /dev/null +++ b/internal/encoding/gzip/handler.go @@ -0,0 +1,59 @@ +package gzip + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "strconv" + + "github.com/aws/aws-sdk-go/aws/request" +) + +// NewGzipRequestHandler provides a named request handler that compresses the +// request payload. Add this to enable GZIP compression for a client. +// +// Known to work with Amazon CloudWatch's PutMetricData operation. +// https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html +func NewGzipRequestHandler() request.NamedHandler { + return request.NamedHandler{ + Name: "GzipRequestHandler", + Fn: gzipRequestHandler, + } +} + +func gzipRequestHandler(req *request.Request) { + compressedBytes, err := compress(req.Body) + if err != nil { + req.Error = fmt.Errorf("failed to compress request payload, %v", err) + return + } + + req.HTTPRequest.Header.Set("Content-Encoding", "gzip") + req.HTTPRequest.Header.Set("Content-Length", strconv.Itoa(len(compressedBytes))) + + req.SetBufferBody(compressedBytes) +} + +func compress(input io.Reader) ([]byte, error) { + var b bytes.Buffer + w, err := gzip.NewWriterLevel(&b, gzip.BestCompression) + if err != nil { + return nil, fmt.Errorf("failed to create gzip writer, %v", err) + } + + inBytes, err := ioutil.ReadAll(input) + if err != nil { + return nil, fmt.Errorf("failed read payload to compress, %v", err) + } + + if _, err = w.Write(inBytes); err != nil { + return nil, fmt.Errorf("failed to write payload to be compressed, %v", err) + } + if err = w.Close(); err != nil { + return nil, fmt.Errorf("failed to flush payload being compressed, %v", err) + } + + return b.Bytes(), nil +} diff --git a/internal/encoding/gzip/handler_test.go b/internal/encoding/gzip/handler_test.go new file mode 100644 index 00000000000..746027fd87b --- /dev/null +++ b/internal/encoding/gzip/handler_test.go @@ -0,0 +1,50 @@ +package gzip + +import ( + "bytes" + "io/ioutil" + "net/http" + "strconv" + "strings" + "testing" + + "github.com/aws/aws-sdk-go/aws/request" +) + +func TestGzipRequestHandler(t *testing.T) { + handler := NewGzipRequestHandler() + + req := &request.Request{} + uncompressed := "asdfasdfasdf" + req.Body = strings.NewReader(uncompressed) + httpReq, err := http.NewRequest("POST", "http://localhost", strings.NewReader(uncompressed)) + if err != nil { + panic(err) + } + req.HTTPRequest = httpReq + + expectCompressed, err := compress(strings.NewReader(uncompressed)) + if err != nil { + t.Fatalf("expect no error, got %v", err) + } + + handler.Fn(req) + if req.Error != nil { + t.Fatalf("expect no error, got %v", req.Error) + } + + if e, a := "gzip", req.HTTPRequest.Header.Get("Content-Encoding"); e != a { + t.Errorf("expect %v content-encoding, got %v", e, a) + } + if e, a := strconv.Itoa(len(expectCompressed)), req.HTTPRequest.Header.Get("Content-Length"); e != a { + t.Errorf("expect %v content-length, got %v", e, a) + } + + actualCompressed, err := ioutil.ReadAll(req.Body) + if err != nil { + t.Fatalf("ReadAll request body failed, %v", err) + } + if !bytes.Equal(expectCompressed, actualCompressed) { + t.Errorf("expect new body to equal expectCompressed") + } +} diff --git a/service/cloudwatch/customizations.go b/service/cloudwatch/customizations.go new file mode 100644 index 00000000000..4c8ec0c0e25 --- /dev/null +++ b/service/cloudwatch/customizations.go @@ -0,0 +1,19 @@ +package cloudwatch + +import ( + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/internal/encoding/gzip" +) + +// WithGzipRequest is a request.Option that adds a request handler to the Build +// stage of the operation's pipeline that will content-encoding GZIP the +// request payload before sending it to the API. This will buffer the request +// payload in memory, GZIP it, and reassign the GZIP'ed payload as the new +// request payload. +// +// GZIP may not be supported by all API operations. See API's documentation for +// the operation your using to see if GZIP request payload is supported. +// https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html +func WithGzipRequest(r *request.Request) { + r.Handlers.Build.PushBackNamed(gzip.NewGzipRequestHandler()) +} diff --git a/service/cloudwatch/customizations_test.go b/service/cloudwatch/customizations_test.go new file mode 100644 index 00000000000..5a40eed326a --- /dev/null +++ b/service/cloudwatch/customizations_test.go @@ -0,0 +1,26 @@ +//go:build go1.7 +// +build go1.7 + +package cloudwatch_test + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/awstesting/unit" + "github.com/aws/aws-sdk-go/service/cloudwatch" +) + +func ExampleCloudWatch_PutMetricDataWithContext_withGzipRequest() { + client := cloudwatch.New(sess) + + // The WithContext form of the operation methods accept request options. + // The WithGzipRequest option will gzip the request payload before it is + // sent. + result, err := client.PutMetricDataWithContext(context.TODO(), params, cloudwatch.WithGzipRequest) + + _, _ = result, err +} + +var params *cloudwatch.PutMetricDataInput +var sess *session.Session = unit.Session