github.com/m-lab/locate@v0.17.6/api/locate/client.go (about)

     1  // Package locate implements a client for the Locate API v2.
     2  package locate
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"flag"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"path"
    13  
    14  	"github.com/m-lab/go/flagx"
    15  	v2 "github.com/m-lab/locate/api/v2"
    16  )
    17  
    18  // ErrNoAvailableServers is returned when there are no available servers. Batch
    19  // clients should pause before scheduling a new request.
    20  var ErrNoAvailableServers = errors.New("no available M-Lab servers")
    21  
    22  // ErrNoUserAgent is returned when an empty user agent is used.
    23  var ErrNoUserAgent = errors.New("client has no user-agent specified")
    24  
    25  // Client is a client for contacting the Locate API. All fields are required.
    26  type Client struct {
    27  	// HTTPClient performs all requests. Initialized to http.DefaultClient by
    28  	// NewClient. You may override it for alternate settings.
    29  	HTTPClient *http.Client
    30  
    31  	// UserAgent is the mandatory user agent to be used. Also this
    32  	// field is initialized by NewClient.
    33  	UserAgent string
    34  
    35  	// BaseURL is the base url used to contact the Locate API.
    36  	// NewClient sets the BaseURL to the -locate.url flag.
    37  	BaseURL *url.URL
    38  }
    39  
    40  // baseURL is the default base URL.
    41  var baseURL = flagx.MustNewURL("https://locate.measurementlab.net/v2/nearest/")
    42  
    43  func init() {
    44  	flag.Var(&baseURL, "locate.url", "The base url for the Locate API")
    45  }
    46  
    47  // NewClient creates a new Client instance. The userAgent must not be empty.
    48  func NewClient(userAgent string) *Client {
    49  	return &Client{
    50  		HTTPClient: http.DefaultClient,
    51  		UserAgent:  userAgent,
    52  		BaseURL:    baseURL.URL,
    53  	}
    54  }
    55  
    56  // Nearest returns a slice of nearby mlab servers. Returns an error on failure.
    57  func (c *Client) Nearest(ctx context.Context, service string) ([]v2.Target, error) {
    58  	var data []byte
    59  	var err error
    60  	var status int
    61  	reqURL := *c.BaseURL
    62  	reqURL.Path = path.Join(reqURL.Path, service)
    63  	data, status, err = c.get(ctx, reqURL.String())
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	reply := &v2.NearestResult{}
    68  	err = json.Unmarshal(data, reply)
    69  	if err != nil {
    70  		// TODO: Distinguish these:
    71  		// * Cloud Endpoint errors have a different JSON structure.
    72  		// * AppEngine 500 gateway failures have no JSON structure.
    73  		return nil, err
    74  	}
    75  	if status != http.StatusOK && reply.Error != nil {
    76  		// TODO: create a derived error using %w.
    77  		return nil, errors.New(reply.Error.Title + ": " + reply.Error.Detail)
    78  	}
    79  	if reply.Results == nil {
    80  		// No explicit error and no results.
    81  		return nil, ErrNoAvailableServers
    82  	}
    83  	return reply.Results, nil
    84  }
    85  
    86  // get is an internal function used to perform the request.
    87  func (c *Client) get(ctx context.Context, URL string) ([]byte, int, error) {
    88  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, URL, nil)
    89  	if err != nil {
    90  		// e.g. due to an invalid parameter.
    91  		return nil, 0, err
    92  	}
    93  	if c.UserAgent == "" {
    94  		// user agent is required.
    95  		return nil, 0, ErrNoUserAgent
    96  	}
    97  	req.Header.Set("User-Agent", c.UserAgent)
    98  	resp, err := c.HTTPClient.Do(req)
    99  	if err != nil {
   100  		return nil, 0, err
   101  	}
   102  	defer resp.Body.Close()
   103  	b, err := ioutil.ReadAll(resp.Body)
   104  	return b, resp.StatusCode, err
   105  }