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 }