github.com/blend/go-sdk@v1.20220411.3/r2/request.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package r2
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"encoding/json"
    14  	"encoding/xml"
    15  	"io"
    16  	"net/http"
    17  	"net/url"
    18  	"time"
    19  
    20  	"github.com/blend/go-sdk/ex"
    21  	"github.com/blend/go-sdk/webutil"
    22  )
    23  
    24  // New returns a new request.
    25  // The default method is GET.
    26  func New(remoteURL string, options ...Option) *Request {
    27  	var r Request
    28  	u, err := url.Parse(remoteURL)
    29  	if err != nil {
    30  		r.Err = ex.New(err)
    31  		return &r
    32  	}
    33  	u.Host = webutil.RemoveHostEmptyPort(u.Host)
    34  	r.Request = &http.Request{
    35  		Method:     MethodGet,
    36  		URL:        u,
    37  		Proto:      "HTTP/1.1",
    38  		ProtoMajor: 1,
    39  		ProtoMinor: 1,
    40  		Header:     make(http.Header),
    41  		Host:       u.Host,
    42  	}
    43  	for _, option := range options {
    44  		if err = option(&r); err != nil {
    45  			r.Err = err
    46  			return &r
    47  		}
    48  	}
    49  	return &r
    50  }
    51  
    52  // Request is a combination of the http.Request options and the underlying client.
    53  type Request struct {
    54  	Request *http.Request
    55  	// Err is an error set on construction.
    56  	// It is checked before sending the request, and will be returned from any of the
    57  	// methods that execute the request.
    58  	// It is typically set in `New(string,...Option)`.
    59  	Err error
    60  	// Client is the underlying http client used to make the requests.
    61  	Client *http.Client
    62  	// Closer is an optional step to run as part of the Close() function.
    63  	Closer func() error
    64  	// Tracer is used to report span contexts to a distributed tracing collector.
    65  	Tracer Tracer
    66  	// OnRequest is an array of request lifecycle hooks used for logging
    67  	// or to modify the request on a per call basis before it is sent.
    68  	OnRequest []OnRequestListener
    69  	// OnResponse is an array of response lifecycle hooks used typically for logging.
    70  	OnResponse []OnResponseListener
    71  }
    72  
    73  // WithContext implements the `WithContext` method for the underlying request.
    74  //
    75  // It is preserved here because the pointer indirects are non-trivial.
    76  func (r *Request) WithContext(ctx context.Context) *Request {
    77  	*r.Request = *r.Request.WithContext(ctx)
    78  	return r
    79  }
    80  
    81  // Do executes the request.
    82  func (r Request) Do() (*http.Response, error) {
    83  	if r.Err != nil {
    84  		return nil, r.Err
    85  	}
    86  	if !webutil.IsValidMethod(r.Request.Method) {
    87  		return nil, ex.New(ErrInvalidMethod, ex.OptMessagef("method: %q", r.Request.Method))
    88  	}
    89  
    90  	if len(r.Request.PostForm) > 0 && r.Request.Body == nil {
    91  		body := r.Request.PostForm.Encode()
    92  		buffer := bytes.NewBufferString(body)
    93  		r.Request.ContentLength = int64(buffer.Len())
    94  		r.Request.Body = io.NopCloser(buffer)
    95  	}
    96  
    97  	if r.Request.Body == nil {
    98  		r.Request.Body = http.NoBody
    99  		r.Request.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(http.NoBody), nil }
   100  	}
   101  
   102  	started := time.Now().UTC()
   103  	var finisher TraceFinisher
   104  	if r.Tracer != nil {
   105  		finisher = r.Tracer.Start(r.Request)
   106  	}
   107  	for _, listener := range r.OnRequest {
   108  		if err := listener(r.Request); err != nil {
   109  			return nil, err
   110  		}
   111  	}
   112  
   113  	var err error
   114  	var res *http.Response
   115  	if r.Client != nil {
   116  		res, err = r.Client.Do(r.Request)
   117  	} else {
   118  		res, err = http.DefaultClient.Do(r.Request)
   119  	}
   120  	if finisher != nil {
   121  		finisher.Finish(r.Request, res, started, err)
   122  	}
   123  	for _, listener := range r.OnResponse {
   124  		if listenerErr := listener(r.Request, res, started, err); listenerErr != nil {
   125  			err = ex.Append(err, listenerErr)
   126  			return nil, err
   127  		}
   128  	}
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	return res, nil
   133  }
   134  
   135  // Close closes the request if there is a closer specified.
   136  func (r *Request) Close() error {
   137  	if r.Closer != nil {
   138  		return r.Closer()
   139  	}
   140  	return nil
   141  }
   142  
   143  // Discard reads the response fully and discards all data it reads, and returns the response metadata.
   144  func (r Request) Discard() (res *http.Response, err error) {
   145  	defer func() {
   146  		if closeErr := r.Close(); closeErr != nil {
   147  			err = ex.Append(err, closeErr)
   148  		}
   149  	}()
   150  
   151  	res, err = r.Do()
   152  	if err != nil {
   153  		res = nil
   154  		return
   155  	}
   156  	defer res.Body.Close()
   157  	_, err = io.Copy(io.Discard, res.Body)
   158  	if err != nil {
   159  		err = ex.New(err)
   160  		return
   161  	}
   162  	return
   163  }
   164  
   165  // CopyTo copies the response body to a given writer.
   166  func (r Request) CopyTo(dst io.Writer) (count int64, err error) {
   167  	defer func() {
   168  		if closeErr := r.Close(); closeErr != nil {
   169  			err = ex.Append(err, closeErr)
   170  		}
   171  	}()
   172  
   173  	var res *http.Response
   174  	res, err = r.Do()
   175  	if err != nil {
   176  		res = nil
   177  		return
   178  	}
   179  	defer res.Body.Close()
   180  	count, err = io.Copy(dst, res.Body)
   181  	if err != nil {
   182  		err = ex.New(err)
   183  		return
   184  	}
   185  	return
   186  }
   187  
   188  // Bytes reads the response and returns it as a byte array, along with the response metadata..
   189  func (r Request) Bytes() (contents []byte, res *http.Response, err error) {
   190  	defer func() {
   191  		if closeErr := r.Close(); closeErr != nil {
   192  			err = ex.Append(err, closeErr)
   193  		}
   194  	}()
   195  	res, err = r.Do()
   196  	if err != nil {
   197  		res = nil
   198  		err = ex.New(err)
   199  		return
   200  	}
   201  	defer func() {
   202  		err = ex.Append(err, res.Body.Close())
   203  	}()
   204  	contents, err = io.ReadAll(res.Body)
   205  	if err != nil {
   206  		err = ex.New(err)
   207  		return
   208  	}
   209  	return
   210  }
   211  
   212  // JSON reads the response as json into a given object and returns the response metadata.
   213  func (r Request) JSON(dst interface{}) (res *http.Response, err error) {
   214  	defer func() {
   215  		if closeErr := r.Close(); closeErr != nil {
   216  			err = ex.Append(err, closeErr)
   217  		}
   218  	}()
   219  
   220  	res, err = r.Do()
   221  	if err != nil {
   222  		res = nil
   223  		err = ex.New(err)
   224  		return
   225  	}
   226  	defer func() {
   227  		err = ex.Append(err, res.Body.Close())
   228  	}()
   229  	if res.StatusCode == http.StatusNoContent {
   230  		err = ex.New(ErrNoContentJSON)
   231  		return
   232  	}
   233  	if err = json.NewDecoder(res.Body).Decode(dst); err != nil {
   234  		err = ex.New(err)
   235  		return
   236  	}
   237  	return
   238  }
   239  
   240  // JSONBytes reads the response as json into a given object
   241  // and returns the response bytes as well as the response metadata.
   242  //
   243  // This method is useful for debugging responses.
   244  func (r Request) JSONBytes(dst interface{}) (body []byte, res *http.Response, err error) {
   245  	defer func() {
   246  		if closeErr := r.Close(); closeErr != nil {
   247  			err = ex.Append(err, closeErr)
   248  		}
   249  	}()
   250  
   251  	res, err = r.Do()
   252  	if err != nil {
   253  		res = nil
   254  		err = ex.New(err)
   255  		return
   256  	}
   257  	defer func() {
   258  		err = ex.Append(err, res.Body.Close())
   259  	}()
   260  	if res.StatusCode == http.StatusNoContent {
   261  		err = ex.New(ErrNoContentJSON)
   262  		return
   263  	}
   264  	body, err = io.ReadAll(res.Body)
   265  	if err != nil {
   266  		err = ex.New(err)
   267  		return
   268  	}
   269  	if err = json.Unmarshal(body, dst); err != nil {
   270  		err = ex.New(err)
   271  		return
   272  	}
   273  	return
   274  }
   275  
   276  // XML reads the response as xml into a given object and returns the response metadata.
   277  func (r Request) XML(dst interface{}) (res *http.Response, err error) {
   278  	defer func() {
   279  		if closeErr := r.Close(); closeErr != nil {
   280  			err = ex.Append(err, closeErr)
   281  		}
   282  	}()
   283  
   284  	res, err = r.Do()
   285  	if err != nil {
   286  		res = nil
   287  		err = ex.New(err)
   288  		return
   289  	}
   290  	defer func() {
   291  		err = ex.Append(err, res.Body.Close())
   292  	}()
   293  	if res.StatusCode == http.StatusNoContent {
   294  		err = ex.New(ErrNoContentXML)
   295  		return
   296  	}
   297  	if err = xml.NewDecoder(res.Body).Decode(dst); err != nil {
   298  		err = ex.New(err)
   299  		return
   300  	}
   301  	return
   302  }