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 }