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  }