1package acceptencoding
2
3import (
4 "compress/gzip"
5 "context"
6 "fmt"
7 "io"
8
9 "github.com/aws/smithy-go"
10 "github.com/aws/smithy-go/middleware"
11 smithyhttp "github.com/aws/smithy-go/transport/http"
12)
13
14const acceptEncodingHeaderKey = "Accept-Encoding"
15const contentEncodingHeaderKey = "Content-Encoding"
16
17// AddAcceptEncodingGzipOptions provides the options for the
18// AddAcceptEncodingGzip middleware setup.
19type AddAcceptEncodingGzipOptions struct {
20 Enable bool
21}
22
23// AddAcceptEncodingGzip explicitly adds handling for accept-encoding GZIP
24// middleware to the operation stack. This allows checksums to be correctly
25// computed without disabling GZIP support.
26func AddAcceptEncodingGzip(stack *middleware.Stack, options AddAcceptEncodingGzipOptions) error {
27 if options.Enable {
28 if err := stack.Finalize.Add(&EnableGzip{}, middleware.Before); err != nil {
29 return err
30 }
31 if err := stack.Deserialize.Insert(&DecompressGzip{}, "OperationDeserializer", middleware.After); err != nil {
32 return err
33 }
34 return nil
35 }
36
37 return stack.Finalize.Add(&DisableGzip{}, middleware.Before)
38}
39
40// DisableGzip provides the middleware that will
41// disable the underlying http client automatically enabling for gzip
42// decompress content-encoding support.
43type DisableGzip struct{}
44
45// ID returns the id for the middleware.
46func (*DisableGzip) ID() string {
47 return "DisableAcceptEncodingGzip"
48}
49
50// HandleFinalize implements the FinalizeMiddleware interface.
51func (*DisableGzip) HandleFinalize(
52 ctx context.Context, input middleware.FinalizeInput, next middleware.FinalizeHandler,
53) (
54 output middleware.FinalizeOutput, metadata middleware.Metadata, err error,
55) {
56 req, ok := input.Request.(*smithyhttp.Request)
57 if !ok {
58 return output, metadata, &smithy.SerializationError{
59 Err: fmt.Errorf("unknown request type %T", input.Request),
60 }
61 }
62
63 // Explicitly enable gzip support, this will prevent the http client from
64 // auto extracting the zipped content.
65 req.Header.Set(acceptEncodingHeaderKey, "identity")
66
67 return next.HandleFinalize(ctx, input)
68}
69
70// EnableGzip provides a middleware to enable support for
71// gzip responses, with manual decompression. This prevents the underlying HTTP
72// client from performing the gzip decompression automatically.
73type EnableGzip struct{}
74
75// ID returns the id for the middleware.
76func (*EnableGzip) ID() string {
77 return "AcceptEncodingGzip"
78}
79
80// HandleFinalize implements the FinalizeMiddleware interface.
81func (*EnableGzip) HandleFinalize(
82 ctx context.Context, input middleware.FinalizeInput, next middleware.FinalizeHandler,
83) (
84 output middleware.FinalizeOutput, metadata middleware.Metadata, err error,
85) {
86 req, ok := input.Request.(*smithyhttp.Request)
87 if !ok {
88 return output, metadata, &smithy.SerializationError{
89 Err: fmt.Errorf("unknown request type %T", input.Request),
90 }
91 }
92
93 // Explicitly enable gzip support, this will prevent the http client from
94 // auto extracting the zipped content.
95 req.Header.Set(acceptEncodingHeaderKey, "gzip")
96
97 return next.HandleFinalize(ctx, input)
98}
99
100// DecompressGzip provides the middleware for decompressing a gzip
101// response from the service.
102type DecompressGzip struct{}
103
104// ID returns the id for the middleware.
105func (*DecompressGzip) ID() string {
106 return "DecompressGzip"
107}
108
109// HandleDeserialize implements the DeserializeMiddlware interface.
110func (*DecompressGzip) HandleDeserialize(
111 ctx context.Context, input middleware.DeserializeInput, next middleware.DeserializeHandler,
112) (
113 output middleware.DeserializeOutput, metadata middleware.Metadata, err error,
114) {
115 output, metadata, err = next.HandleDeserialize(ctx, input)
116 if err != nil {
117 return output, metadata, err
118 }
119
120 resp, ok := output.RawResponse.(*smithyhttp.Response)
121 if !ok {
122 return output, metadata, &smithy.DeserializationError{
123 Err: fmt.Errorf("unknown response type %T", output.RawResponse),
124 }
125 }
126 if v := resp.Header.Get(contentEncodingHeaderKey); v != "gzip" {
127 return output, metadata, err
128 }
129
130 // Clear content length since it will no longer be valid once the response
131 // body is decompressed.
132 resp.Header.Del("Content-Length")
133 resp.ContentLength = -1
134
135 resp.Body = wrapGzipReader(resp.Body)
136
137 return output, metadata, err
138}
139
140type gzipReader struct {
141 reader io.ReadCloser
142 gzip *gzip.Reader
143}
144
145func wrapGzipReader(reader io.ReadCloser) *gzipReader {
146 return &gzipReader{
147 reader: reader,
148 }
149}
150
151// Read wraps the gzip reader around the underlying io.Reader to extract the
152// response bytes on the fly.
153func (g *gzipReader) Read(b []byte) (n int, err error) {
154 if g.gzip == nil {
155 g.gzip, err = gzip.NewReader(g.reader)
156 if err != nil {
157 g.gzip = nil // ensure uninitialized gzip value isn't used in close.
158 return 0, fmt.Errorf("failed to decompress gzip response, %w", err)
159 }
160 }
161
162 return g.gzip.Read(b)
163}
164
165func (g *gzipReader) Close() error {
166 if g.gzip == nil {
167 return nil
168 }
169
170 if err := g.gzip.Close(); err != nil {
171 g.reader.Close()
172 return fmt.Errorf("failed to decompress gzip response, %w", err)
173 }
174
175 return g.reader.Close()
176}