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 }