request.go raw

   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  }
 189