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