request.go

  1package http
  2
  3import (
  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.
 17type 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.
 27func 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.
 38func (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.
 47func (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.
 56func (r *Request) StreamLength() (size int64, ok bool, err error) {
 57	return streamLength(r.stream, r.isStreamSeekable, r.streamStartPos)
 58}
 59
 60func 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.
 95func (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.
110func (r *Request) GetStream() io.Reader {
111	return r.stream
112}
113
114// IsStreamSeekable returns whether the stream is seekable.
115func (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.
122func (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.
161func (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.
187func RequestCloner(v interface{}) interface{} {
188	return v.(*Request).Clone()
189}