2021-10-17 17:15:44 +00:00
|
|
|
package http
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
|
|
|
|
iointernal "github.com/aws/smithy-go/transport/http/internal/io"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Request provides the HTTP specific request structure for HTTP specific
|
|
|
|
// middleware steps to use to serialize input, and send an operation's request.
|
|
|
|
type Request struct {
|
|
|
|
*http.Request
|
|
|
|
stream io.Reader
|
|
|
|
isStreamSeekable bool
|
|
|
|
streamStartPos int64
|
|
|
|
}
|
|
|
|
|
2022-06-09 10:30:53 +00:00
|
|
|
// NewStackRequest returns an initialized request ready to be populated with the
|
2021-10-17 17:15:44 +00:00
|
|
|
// HTTP request details. Returns empty interface so the function can be used as
|
|
|
|
// a parameter to the Smithy middleware Stack constructor.
|
|
|
|
func NewStackRequest() interface{} {
|
|
|
|
return &Request{
|
|
|
|
Request: &http.Request{
|
|
|
|
URL: &url.URL{},
|
|
|
|
Header: http.Header{},
|
|
|
|
ContentLength: -1, // default to unknown length
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clone returns a deep copy of the Request for the new context. A reference to
|
|
|
|
// the Stream is copied, but the underlying stream is not copied.
|
|
|
|
func (r *Request) Clone() *Request {
|
|
|
|
rc := *r
|
|
|
|
rc.Request = rc.Request.Clone(context.TODO())
|
|
|
|
return &rc
|
|
|
|
}
|
|
|
|
|
|
|
|
// StreamLength returns the number of bytes of the serialized stream attached
|
|
|
|
// to the request and ok set. If the length cannot be determined, an error will
|
|
|
|
// be returned.
|
|
|
|
func (r *Request) StreamLength() (size int64, ok bool, err error) {
|
2022-06-09 10:30:53 +00:00
|
|
|
return streamLength(r.stream, r.isStreamSeekable, r.streamStartPos)
|
|
|
|
}
|
|
|
|
|
|
|
|
func streamLength(stream io.Reader, seekable bool, startPos int64) (size int64, ok bool, err error) {
|
|
|
|
if stream == nil {
|
2021-10-17 17:15:44 +00:00
|
|
|
return 0, true, nil
|
|
|
|
}
|
|
|
|
|
2022-06-09 10:30:53 +00:00
|
|
|
if l, ok := stream.(interface{ Len() int }); ok {
|
2021-10-17 17:15:44 +00:00
|
|
|
return int64(l.Len()), true, nil
|
|
|
|
}
|
|
|
|
|
2022-06-09 10:30:53 +00:00
|
|
|
if !seekable {
|
2021-10-17 17:15:44 +00:00
|
|
|
return 0, false, nil
|
|
|
|
}
|
|
|
|
|
2022-06-09 10:30:53 +00:00
|
|
|
s := stream.(io.Seeker)
|
2021-10-17 17:15:44 +00:00
|
|
|
endOffset, err := s.Seek(0, io.SeekEnd)
|
|
|
|
if err != nil {
|
|
|
|
return 0, false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// The reason to seek to streamStartPos instead of 0 is to ensure that the
|
|
|
|
// SDK only sends the stream from the starting position the user's
|
|
|
|
// application provided it to the SDK at. For example application opens a
|
|
|
|
// file, and wants to skip the first N bytes uploading the rest. The
|
|
|
|
// application would move the file's offset N bytes, then hand it off to
|
|
|
|
// the SDK to send the remaining. The SDK should respect that initial offset.
|
2022-06-09 10:30:53 +00:00
|
|
|
_, err = s.Seek(startPos, io.SeekStart)
|
2021-10-17 17:15:44 +00:00
|
|
|
if err != nil {
|
|
|
|
return 0, false, err
|
|
|
|
}
|
|
|
|
|
2022-06-09 10:30:53 +00:00
|
|
|
return endOffset - startPos, true, nil
|
2021-10-17 17:15:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// RewindStream will rewind the io.Reader to the relative start position if it
|
|
|
|
// is an io.Seeker.
|
|
|
|
func (r *Request) RewindStream() error {
|
|
|
|
// If there is no stream there is nothing to rewind.
|
|
|
|
if r.stream == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
if !r.isStreamSeekable {
|
|
|
|
return fmt.Errorf("request stream is not seekable")
|
|
|
|
}
|
|
|
|
_, err := r.stream.(io.Seeker).Seek(r.streamStartPos, io.SeekStart)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetStream returns the request stream io.Reader if a stream is set. If no
|
|
|
|
// stream is present nil will be returned.
|
|
|
|
func (r *Request) GetStream() io.Reader {
|
|
|
|
return r.stream
|
|
|
|
}
|
|
|
|
|
2022-06-09 10:30:53 +00:00
|
|
|
// IsStreamSeekable returns whether the stream is seekable.
|
2021-10-17 17:15:44 +00:00
|
|
|
func (r *Request) IsStreamSeekable() bool {
|
|
|
|
return r.isStreamSeekable
|
|
|
|
}
|
|
|
|
|
2022-06-09 10:30:53 +00:00
|
|
|
// SetStream returns a clone of the request with the stream set to the provided
|
|
|
|
// reader. May return an error if the provided reader is seekable but returns
|
|
|
|
// an error.
|
2021-10-17 17:15:44 +00:00
|
|
|
func (r *Request) SetStream(reader io.Reader) (rc *Request, err error) {
|
|
|
|
rc = r.Clone()
|
|
|
|
|
2022-06-09 10:30:53 +00:00
|
|
|
if reader == http.NoBody {
|
|
|
|
reader = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var isStreamSeekable bool
|
|
|
|
var streamStartPos int64
|
2021-10-17 17:15:44 +00:00
|
|
|
switch v := reader.(type) {
|
|
|
|
case io.Seeker:
|
|
|
|
n, err := v.Seek(0, io.SeekCurrent)
|
|
|
|
if err != nil {
|
|
|
|
return r, err
|
|
|
|
}
|
2022-06-09 10:30:53 +00:00
|
|
|
isStreamSeekable = true
|
|
|
|
streamStartPos = n
|
2021-10-17 17:15:44 +00:00
|
|
|
default:
|
2022-06-09 10:30:53 +00:00
|
|
|
// If the stream length can be determined, and is determined to be empty,
|
|
|
|
// use a nil stream to prevent confusion between empty vs not-empty
|
|
|
|
// streams.
|
|
|
|
length, ok, err := streamLength(reader, false, 0)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
} else if ok && length == 0 {
|
|
|
|
reader = nil
|
|
|
|
}
|
2021-10-17 17:15:44 +00:00
|
|
|
}
|
2022-06-09 10:30:53 +00:00
|
|
|
|
2021-10-17 17:15:44 +00:00
|
|
|
rc.stream = reader
|
2022-06-09 10:30:53 +00:00
|
|
|
rc.isStreamSeekable = isStreamSeekable
|
|
|
|
rc.streamStartPos = streamStartPos
|
2021-10-17 17:15:44 +00:00
|
|
|
|
|
|
|
return rc, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Build returns a build standard HTTP request value from the Smithy request.
|
|
|
|
// The request's stream is wrapped in a safe container that allows it to be
|
|
|
|
// reused for subsequent attempts.
|
|
|
|
func (r *Request) Build(ctx context.Context) *http.Request {
|
|
|
|
req := r.Request.Clone(ctx)
|
|
|
|
|
2021-11-24 18:10:52 +00:00
|
|
|
if r.stream == nil && req.ContentLength == -1 {
|
2021-10-17 17:15:44 +00:00
|
|
|
req.ContentLength = 0
|
|
|
|
}
|
|
|
|
|
2021-11-24 18:10:52 +00:00
|
|
|
switch stream := r.stream.(type) {
|
|
|
|
case *io.PipeReader:
|
|
|
|
req.Body = ioutil.NopCloser(stream)
|
|
|
|
req.ContentLength = -1
|
|
|
|
default:
|
2022-06-09 10:30:53 +00:00
|
|
|
// HTTP Client Request must only have a non-nil body if the
|
|
|
|
// ContentLength is explicitly unknown (-1) or non-zero. The HTTP
|
|
|
|
// Client will interpret a non-nil body and ContentLength 0 as
|
|
|
|
// "unknown". This is unwanted behavior.
|
|
|
|
if req.ContentLength != 0 && r.stream != nil {
|
2021-11-24 18:10:52 +00:00
|
|
|
req.Body = iointernal.NewSafeReadCloser(ioutil.NopCloser(stream))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-17 17:15:44 +00:00
|
|
|
return req
|
|
|
|
}
|
|
|
|
|
|
|
|
// RequestCloner is a function that can take an input request type and clone the request
|
2022-06-09 10:30:53 +00:00
|
|
|
// for use in a subsequent retry attempt.
|
2021-10-17 17:15:44 +00:00
|
|
|
func RequestCloner(v interface{}) interface{} {
|
|
|
|
return v.(*Request).Clone()
|
|
|
|
}
|