decred.org/dcrdex@v1.0.5/dex/dexnet/http.go (about) 1 // This code is available on the terms of the project LICENSE.md file, 2 // also available online at https://blueoakcouncil.org/license/1.0.0. 3 4 package dexnet 5 6 import ( 7 "bytes" 8 "context" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 ) 14 15 const defaultResponseSizeLimit = 1 << 20 // 1 MiB = 1,048,576 bytes 16 17 // RequestOption are optional arguemnts to Get, Post, or Do. 18 type RequestOption struct { 19 responseSizeLimit int64 20 statusFunc func(int) 21 header *[2]string 22 errThing interface{} 23 } 24 25 // WithSizeLimit sets a size limit for a response. See defaultResponseSizeLimit 26 // for the default. 27 func WithSizeLimit(limit int64) *RequestOption { 28 return &RequestOption{responseSizeLimit: limit} 29 } 30 31 // WithStatusFunc calls a function with the status code after the request is 32 // performed. 33 func WithStatusFunc(f func(int)) *RequestOption { 34 return &RequestOption{statusFunc: f} 35 } 36 37 // WithRequestHeader adds a header entry to the request. 38 func WithRequestHeader(k, v string) *RequestOption { 39 h := [2]string{k, v} 40 return &RequestOption{header: &h} 41 } 42 43 // WithErrorParsing adds parsing of response bodies for HTTP error responses. 44 func WithErrorParsing(thing interface{}) *RequestOption { 45 return &RequestOption{errThing: thing} 46 } 47 48 // Post peforms an HTTP POST request. If thing is non-nil, the response will 49 // be JSON-unmarshaled into thing. 50 func Post(ctx context.Context, uri string, thing interface{}, body []byte, opts ...*RequestOption) error { 51 var r io.Reader 52 if len(body) == 1 { 53 r = bytes.NewReader(body) 54 } 55 req, err := http.NewRequestWithContext(ctx, http.MethodPost, uri, r) 56 if err != nil { 57 return fmt.Errorf("error constructing request: %w", err) 58 } 59 return Do(req, thing, opts...) 60 } 61 62 // Post peforms an HTTP GET request. If thing is non-nil, the response will 63 // be JSON-unmarshaled into thing. 64 func Get(ctx context.Context, uri string, thing interface{}, opts ...*RequestOption) error { 65 req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) 66 if err != nil { 67 return fmt.Errorf("error constructing request: %w", err) 68 } 69 return Do(req, thing, opts...) 70 } 71 72 // Do does the request and JSON-marshals the result into thing, if non-nil. 73 func Do(req *http.Request, thing interface{}, opts ...*RequestOption) error { 74 var sizeLimit int64 = defaultResponseSizeLimit 75 var statusFunc func(int) 76 var errThing interface{} 77 for _, opt := range opts { 78 switch { 79 case opt.responseSizeLimit > 0: 80 sizeLimit = opt.responseSizeLimit 81 case opt.statusFunc != nil: 82 statusFunc = opt.statusFunc 83 case opt.header != nil: 84 h := *opt.header 85 k, v := h[0], h[1] 86 req.Header.Add(k, v) 87 case opt.errThing != nil: 88 errThing = opt.errThing 89 } 90 } 91 resp, err := http.DefaultClient.Do(req) 92 if err != nil { 93 return fmt.Errorf("error performing request: %w", err) 94 } 95 defer resp.Body.Close() 96 if statusFunc != nil { 97 statusFunc(resp.StatusCode) 98 } 99 if resp.StatusCode != http.StatusOK { 100 if errThing != nil { 101 reader := io.LimitReader(resp.Body, sizeLimit) 102 if err = json.NewDecoder(reader).Decode(errThing); err != nil { 103 return fmt.Errorf("HTTP error: %q (code %d). error encountered parsing error body: %w", resp.Status, resp.StatusCode, err) 104 } 105 } 106 return fmt.Errorf("HTTP error: %q (code %d)", resp.Status, resp.StatusCode) 107 } 108 if thing == nil { 109 return nil 110 } 111 reader := io.LimitReader(resp.Body, sizeLimit) 112 if err = json.NewDecoder(reader).Decode(thing); err != nil { 113 return fmt.Errorf("error decoding request: %w", err) 114 } 115 return nil 116 }