request.go (5331B)
1 package http 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "net/url" 9 "strings" 10 11 iointernal "github.com/aws/smithy-go/transport/http/internal/io" 12 ) 13 14 // Request provides the HTTP specific request structure for HTTP specific 15 // middleware steps to use to serialize input, and send an operation's request. 16 type Request struct { 17 *http.Request 18 stream io.Reader 19 isStreamSeekable bool 20 streamStartPos int64 21 } 22 23 // NewStackRequest returns an initialized request ready to be populated with the 24 // HTTP request details. Returns empty interface so the function can be used as 25 // a parameter to the Smithy middleware Stack constructor. 26 func NewStackRequest() interface{} { 27 return &Request{ 28 Request: &http.Request{ 29 URL: &url.URL{}, 30 Header: http.Header{}, 31 ContentLength: -1, // default to unknown length 32 }, 33 } 34 } 35 36 // IsHTTPS returns if the request is HTTPS. Returns false if no endpoint URL is set. 37 func (r *Request) IsHTTPS() bool { 38 if r.URL == nil { 39 return false 40 } 41 return strings.EqualFold(r.URL.Scheme, "https") 42 } 43 44 // Clone returns a deep copy of the Request for the new context. A reference to 45 // the Stream is copied, but the underlying stream is not copied. 46 func (r *Request) Clone() *Request { 47 rc := *r 48 rc.Request = rc.Request.Clone(context.TODO()) 49 return &rc 50 } 51 52 // StreamLength returns the number of bytes of the serialized stream attached 53 // to the request and ok set. If the length cannot be determined, an error will 54 // be returned. 55 func (r *Request) StreamLength() (size int64, ok bool, err error) { 56 return streamLength(r.stream, r.isStreamSeekable, r.streamStartPos) 57 } 58 59 func streamLength(stream io.Reader, seekable bool, startPos int64) (size int64, ok bool, err error) { 60 if stream == nil { 61 return 0, true, nil 62 } 63 64 if l, ok := stream.(interface{ Len() int }); ok { 65 return int64(l.Len()), true, nil 66 } 67 68 if !seekable { 69 return 0, false, nil 70 } 71 72 s := stream.(io.Seeker) 73 endOffset, err := s.Seek(0, io.SeekEnd) 74 if err != nil { 75 return 0, false, err 76 } 77 78 // The reason to seek to streamStartPos instead of 0 is to ensure that the 79 // SDK only sends the stream from the starting position the user's 80 // application provided it to the SDK at. For example application opens a 81 // file, and wants to skip the first N bytes uploading the rest. The 82 // application would move the file's offset N bytes, then hand it off to 83 // the SDK to send the remaining. The SDK should respect that initial offset. 84 _, err = s.Seek(startPos, io.SeekStart) 85 if err != nil { 86 return 0, false, err 87 } 88 89 return endOffset - startPos, true, nil 90 } 91 92 // RewindStream will rewind the io.Reader to the relative start position if it 93 // is an io.Seeker. 94 func (r *Request) RewindStream() error { 95 // If there is no stream there is nothing to rewind. 96 if r.stream == nil { 97 return nil 98 } 99 100 if !r.isStreamSeekable { 101 return fmt.Errorf("request stream is not seekable") 102 } 103 _, err := r.stream.(io.Seeker).Seek(r.streamStartPos, io.SeekStart) 104 return err 105 } 106 107 // GetStream returns the request stream io.Reader if a stream is set. If no 108 // stream is present nil will be returned. 109 func (r *Request) GetStream() io.Reader { 110 return r.stream 111 } 112 113 // IsStreamSeekable returns whether the stream is seekable. 114 func (r *Request) IsStreamSeekable() bool { 115 return r.isStreamSeekable 116 } 117 118 // SetStream returns a clone of the request with the stream set to the provided 119 // reader. May return an error if the provided reader is seekable but returns 120 // an error. 121 func (r *Request) SetStream(reader io.Reader) (rc *Request, err error) { 122 rc = r.Clone() 123 124 if reader == http.NoBody { 125 reader = nil 126 } 127 128 var isStreamSeekable bool 129 var streamStartPos int64 130 switch v := reader.(type) { 131 case io.Seeker: 132 n, err := v.Seek(0, io.SeekCurrent) 133 if err != nil { 134 return r, err 135 } 136 isStreamSeekable = true 137 streamStartPos = n 138 default: 139 // If the stream length can be determined, and is determined to be empty, 140 // use a nil stream to prevent confusion between empty vs not-empty 141 // streams. 142 length, ok, err := streamLength(reader, false, 0) 143 if err != nil { 144 return nil, err 145 } else if ok && length == 0 { 146 reader = nil 147 } 148 } 149 150 rc.stream = reader 151 rc.isStreamSeekable = isStreamSeekable 152 rc.streamStartPos = streamStartPos 153 154 return rc, err 155 } 156 157 // Build returns a build standard HTTP request value from the Smithy request. 158 // The request's stream is wrapped in a safe container that allows it to be 159 // reused for subsequent attempts. 160 func (r *Request) Build(ctx context.Context) *http.Request { 161 req := r.Request.Clone(ctx) 162 163 if r.stream == nil && req.ContentLength == -1 { 164 req.ContentLength = 0 165 } 166 167 switch stream := r.stream.(type) { 168 case *io.PipeReader: 169 req.Body = io.NopCloser(stream) 170 req.ContentLength = -1 171 default: 172 // HTTP Client Request must only have a non-nil body if the 173 // ContentLength is explicitly unknown (-1) or non-zero. The HTTP 174 // Client will interpret a non-nil body and ContentLength 0 as 175 // "unknown". This is unwanted behavior. 176 if req.ContentLength != 0 && r.stream != nil { 177 req.Body = iointernal.NewSafeReadCloser(io.NopCloser(stream)) 178 } 179 } 180 181 return req 182 } 183 184 // RequestCloner is a function that can take an input request type and clone the request 185 // for use in a subsequent retry attempt. 186 func RequestCloner(v interface{}) interface{} { 187 return v.(*Request).Clone() 188 }