go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/r2/request.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package r2
     9  
    10  import (
    11  	"bytes"
    12  	"context"
    13  	"encoding/gob"
    14  	"encoding/json"
    15  	"io"
    16  	"net/http"
    17  	"net/url"
    18  	"time"
    19  
    20  	"go.charczuk.com/sdk/errutil"
    21  )
    22  
    23  // New returns a new request.
    24  //
    25  // The default method is GET, the default Proto is `HTTP/1.1`.
    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 = err
    31  		return &r
    32  	}
    33  	u.Host = RemoveHostEmptyPort(u.Host)
    34  	r.req = &http.Request{
    35  		Method:     http.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  	req        *http.Request
    55  	err        error
    56  	client     *http.Client
    57  	closer     func() error
    58  	onRequest  []func(*http.Request) error
    59  	onResponse []func(*http.Request, *http.Response, time.Time, error) error
    60  }
    61  
    62  // WithContext implements the `WithContext` method for the underlying request.
    63  //
    64  // It is preserved here because the pointer indirects are non-trivial.
    65  func (r *Request) WithContext(ctx context.Context) *Request {
    66  	*r.req = *r.req.WithContext(ctx)
    67  	return r
    68  }
    69  
    70  // Method returns the request method.
    71  func (r *Request) Method() string {
    72  	return r.req.Method
    73  }
    74  
    75  // URL returns the request url.
    76  func (r *Request) URL() *url.URL {
    77  	return r.req.URL
    78  }
    79  
    80  // Do executes the request.
    81  func (r Request) Do() (*http.Response, error) {
    82  	if r.err != nil {
    83  		return nil, r.err
    84  	}
    85  	if len(r.req.PostForm) > 0 {
    86  		if r.req.Body != nil {
    87  			return nil, ErrFormAndBodySet
    88  		}
    89  		body := r.req.PostForm.Encode()
    90  		buffer := bytes.NewBufferString(body)
    91  		r.req.ContentLength = int64(buffer.Len())
    92  		r.req.Body = io.NopCloser(buffer)
    93  	}
    94  
    95  	if r.req.Body == nil {
    96  		r.req.Body = http.NoBody
    97  		r.req.GetBody = func() (io.ReadCloser, error) { return io.NopCloser(http.NoBody), nil }
    98  	}
    99  
   100  	started := time.Now().UTC()
   101  	for _, listener := range r.onRequest {
   102  		if err := listener(r.req); err != nil {
   103  			return nil, err
   104  		}
   105  	}
   106  
   107  	var err error
   108  	var res *http.Response
   109  	if r.client != nil {
   110  		res, err = r.client.Do(r.req)
   111  	} else {
   112  		res, err = http.DefaultClient.Do(r.req)
   113  	}
   114  	for _, listener := range r.onResponse {
   115  		if listenerErr := listener(r.req, res, started, err); listenerErr != nil {
   116  			return nil, listenerErr
   117  		}
   118  	}
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	return res, nil
   123  }
   124  
   125  // Close closes the request if there is a closer specified.
   126  func (r *Request) Close() error {
   127  	if r.closer != nil {
   128  		return r.closer()
   129  	}
   130  	return nil
   131  }
   132  
   133  // Discard reads the response fully and discards all data it reads, and returns the response metadata.
   134  func (r Request) Discard() (res *http.Response, err error) {
   135  	defer func() {
   136  		if closeErr := r.Close(); closeErr != nil {
   137  			err = errutil.Append(err, closeErr)
   138  		}
   139  	}()
   140  	res, err = r.Do()
   141  	if err != nil {
   142  		res = nil
   143  		return
   144  	}
   145  	defer res.Body.Close()
   146  	_, err = io.Copy(io.Discard, res.Body)
   147  	return
   148  }
   149  
   150  // CopyTo copies the response body to a given writer.
   151  func (r Request) CopyTo(dst io.Writer) (count int64, err error) {
   152  	defer func() {
   153  		if closeErr := r.Close(); closeErr != nil {
   154  			err = errutil.Append(err, closeErr)
   155  		}
   156  	}()
   157  
   158  	var res *http.Response
   159  	res, err = r.Do()
   160  	if err != nil {
   161  		res = nil
   162  		return
   163  	}
   164  	defer res.Body.Close()
   165  	count, err = io.Copy(dst, res.Body)
   166  	return
   167  }
   168  
   169  // Bytes reads the response and returns it as a byte array, along with the response metadata..
   170  func (r Request) Bytes() (contents []byte, res *http.Response, err error) {
   171  	defer func() {
   172  		if closeErr := r.Close(); closeErr != nil {
   173  			err = errutil.Append(err, closeErr)
   174  		}
   175  	}()
   176  	res, err = r.Do()
   177  	if err != nil {
   178  		res = nil
   179  		return
   180  	}
   181  	defer res.Body.Close()
   182  	contents, err = io.ReadAll(res.Body)
   183  	return
   184  }
   185  
   186  // JSON reads the response as json into a given object and returns the response metadata.
   187  func (r Request) JSON(dst interface{}) (res *http.Response, err error) {
   188  	defer func() {
   189  		if closeErr := r.Close(); closeErr != nil {
   190  			err = errutil.Append(err, closeErr)
   191  		}
   192  	}()
   193  
   194  	res, err = r.Do()
   195  	if err != nil {
   196  		res = nil
   197  		return
   198  	}
   199  	defer res.Body.Close()
   200  	if res.StatusCode == http.StatusNoContent {
   201  		err = ErrNoContentJSON
   202  		return
   203  	}
   204  	if err = json.NewDecoder(res.Body).Decode(dst); err != nil {
   205  		return
   206  	}
   207  	return
   208  }
   209  
   210  // Gob reads the response as gob into a given object and returns the response metadata.
   211  func (r Request) Gob(dst interface{}) (res *http.Response, err error) {
   212  	defer func() {
   213  		if closeErr := r.Close(); closeErr != nil {
   214  			err = errutil.Append(err, closeErr)
   215  		}
   216  	}()
   217  
   218  	res, err = r.Do()
   219  	if err != nil {
   220  		res = nil
   221  		return
   222  	}
   223  	defer res.Body.Close()
   224  	if res.StatusCode == http.StatusNoContent {
   225  		err = ErrNoContentGob
   226  		return
   227  	}
   228  	if err = gob.NewDecoder(res.Body).Decode(dst); err != nil {
   229  		return
   230  	}
   231  	return
   232  }