Skip to content

Commit

Permalink
Implement guidelines (#46)
Browse files Browse the repository at this point in the history
* set user agent

* 1 second poll interval + set Accept-Enconding header

* add Accept-Encoding GET requests

* brotli decode + etag implementation

* add tests

* fix brotli post, add test

* catch other get errors
  • Loading branch information
didil authored Apr 11, 2023
1 parent c0c5052 commit dea6d12
Show file tree
Hide file tree
Showing 19 changed files with 351 additions and 135 deletions.
88 changes: 10 additions & 78 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import (
"fmt"
"io"
"net/http"
"runtime"

"github.com/andybalholm/brotli"
"github.com/jsdelivr/globalping-cli/model"
)

const userAgent = "Globalping API Go Client / v1" + " (" + runtime.GOOS + "/" + runtime.GOARCH + ")"

var ApiUrl = "https://api.globalping.io/v1/measurements"

// Post measurement to Globalping API - boolean indicates whether to print CLI help on error
Expand All @@ -29,7 +27,8 @@ func PostAPI(measurement model.PostMeasurement) (model.PostResponse, bool, error
if err != nil {
return model.PostResponse{}, false, errors.New("err: failed to create request - please report this bug")
}
req.Header.Set("User-Agent", userAgent)
req.Header.Set("User-Agent", userAgent())
req.Header.Set("Accept-Encoding", "br")
req.Header.Set("Content-Type", "application/json")

// Make the request
Expand Down Expand Up @@ -73,8 +72,14 @@ func PostAPI(measurement model.PostMeasurement) (model.PostResponse, bool, error
}

// Read the response body

var bodyReader io.Reader = resp.Body
if resp.Header.Get("Content-Encoding") == "br" {
bodyReader = brotli.NewReader(bodyReader)
}

var data model.PostResponse
err = json.NewDecoder(resp.Body).Decode(&data)
err = json.NewDecoder(bodyReader).Decode(&data)
if err != nil {
fmt.Println(err)
return model.PostResponse{}, false, errors.New("err: invalid post measurement format returned - please report this bug")
Expand All @@ -100,76 +105,3 @@ func DecodeTimings(cmd string, timings json.RawMessage) (model.Timings, error) {

return data, nil
}

// Get measurement from Globalping API
func GetAPI(id string) (model.GetMeasurement, error) {
// Create a new request
req, err := http.NewRequest("GET", ApiUrl+"/"+id, nil)
if err != nil {
return model.GetMeasurement{}, errors.New("err: failed to create request")
}
req.Header.Set("User-Agent", userAgent)

// Make the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return model.GetMeasurement{}, errors.New("err: request failed")
}
defer resp.Body.Close()

// 404 not found
if resp.StatusCode == http.StatusNotFound {
return model.GetMeasurement{}, errors.New("err: measurement not found")
}

// 500 error
if resp.StatusCode == http.StatusInternalServerError {
return model.GetMeasurement{}, errors.New("err: internal server error - please try again later")
}

// Read the response body
var data model.GetMeasurement
err = json.NewDecoder(resp.Body).Decode(&data)
if err != nil {
return model.GetMeasurement{}, errors.New("invalid get measurement format returned")
}

return data, nil
}

func GetApiJson(id string) (string, error) {
// Create a new request
req, err := http.NewRequest("GET", ApiUrl+"/"+id, nil)
if err != nil {
return "", errors.New("err: failed to create request")
}
req.Header.Set("User-Agent", userAgent)

// Make the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", errors.New("err: request failed")
}
defer resp.Body.Close()

// 404 not found
if resp.StatusCode == http.StatusNotFound {
return "", errors.New("err: measurement not found")
}

// 500 error
if resp.StatusCode == http.StatusInternalServerError {
return "", errors.New("err: internal server error - please try again later")
}

// Read the response body
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.New("err: failed to read response body")
}
respString := string(respBytes)

return respString, nil
}
30 changes: 22 additions & 8 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,9 @@ func testGetValid(t *testing.T) {
defer server.Close()
client.ApiUrl = server.URL

res, err := client.GetAPI("abcd")
fetcher := client.NewMeasurementsFetcher(server.URL)

res, err := fetcher.GetMeasurement("abcd")
if err != nil {
t.Error(err)
}
Expand All @@ -152,12 +154,14 @@ func testGetJson(t *testing.T) {
defer server.Close()
client.ApiUrl = server.URL

res, err := client.GetApiJson("abcd")
fetcher := client.NewMeasurementsFetcher(server.URL)

res, err := fetcher.GetRawMeasurement("abcd")
if err != nil {
t.Error(err)
}

assert.Equal(t, `{"id":"abcd"}`, res)
assert.Equal(t, `{"id":"abcd"}`, string(res))
}

func testGetPing(t *testing.T) {
Expand Down Expand Up @@ -206,7 +210,9 @@ func testGetPing(t *testing.T) {
defer server.Close()
client.ApiUrl = server.URL

res, err := client.GetAPI("abcd")
fetcher := client.NewMeasurementsFetcher(server.URL)

res, err := fetcher.GetMeasurement("abcd")
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -299,7 +305,9 @@ func testGetTraceroute(t *testing.T) {
defer server.Close()
client.ApiUrl = server.URL

res, err := client.GetAPI("abcd")
fetcher := client.NewMeasurementsFetcher(server.URL)

res, err := fetcher.GetMeasurement("abcd")
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -375,7 +383,9 @@ func testGetDns(t *testing.T) {
defer server.Close()
client.ApiUrl = server.URL

res, err := client.GetAPI("abcd")
fetcher := client.NewMeasurementsFetcher(server.URL)

res, err := fetcher.GetMeasurement("abcd")
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -498,7 +508,9 @@ func testGetMtr(t *testing.T) {
defer server.Close()
client.ApiUrl = server.URL

res, err := client.GetAPI("abcd")
fetcher := client.NewMeasurementsFetcher(server.URL)

res, err := fetcher.GetMeasurement("abcd")
if err != nil {
t.Error(err)
}
Expand Down Expand Up @@ -603,7 +615,9 @@ func testGetHttp(t *testing.T) {
defer server.Close()
client.ApiUrl = server.URL

res, err := client.GetAPI("abcd")
fetcher := client.NewMeasurementsFetcher(server.URL)

res, err := fetcher.GetMeasurement("abcd")
if err != nil {
t.Error(err)
}
Expand Down
124 changes: 124 additions & 0 deletions client/measurements_fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package client

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

"github.com/andybalholm/brotli"
"github.com/jsdelivr/globalping-cli/model"
)

type MeasurementsFetcher interface {
GetMeasurement(id string) (*model.GetMeasurement, error)
GetRawMeasurement(id string) ([]byte, error)
}

type measurementsFetcher struct {
// The api url endpoint
apiUrl string

// http client
cl *http.Client

// caches Etags by measurement id
etags map[string]string

// caches Measurements by ETag
measurements map[string][]byte
}

func NewMeasurementsFetcher(apiUrl string) *measurementsFetcher {
return &measurementsFetcher{
apiUrl: apiUrl,
cl: &http.Client{},
etags: map[string]string{},
measurements: map[string][]byte{},
}
}

// GetRawMeasurement returns API response as a GetMeasurement object
func (f *measurementsFetcher) GetMeasurement(id string) (*model.GetMeasurement, error) {
respBytes, err := f.GetRawMeasurement(id)
if err != nil {
return nil, err
}

var m model.GetMeasurement
err = json.Unmarshal(respBytes, &m)
if err != nil {
return nil, fmt.Errorf("invalid get measurement format returned: %v %s", err, string(respBytes))
}

return &m, nil
}

// GetRawMeasurement returns the API response's raw json response
func (f *measurementsFetcher) GetRawMeasurement(id string) ([]byte, error) {
// Create a new request
req, err := http.NewRequest("GET", f.apiUrl+"/"+id, nil)
if err != nil {
return nil, errors.New("err: failed to create request")
}

req.Header.Set("User-Agent", userAgent())
req.Header.Set("Accept-Encoding", "br")

etag := f.etags[id]
if etag != "" {
req.Header.Set("If-None-Match", etag)
}

// Make the request
resp, err := f.cl.Do(req)
if err != nil {
return nil, errors.New("err: request failed")
}
defer resp.Body.Close()

// 404 not found
if resp.StatusCode == http.StatusNotFound {
return nil, errors.New("err: measurement not found")
}

// 500 error
if resp.StatusCode == http.StatusInternalServerError {
return nil, errors.New("err: internal server error - please try again later")
}

// 304 not modified
if resp.StatusCode == http.StatusNotModified {
// get response bytes from cache
respBytes := f.measurements[etag]
if respBytes == nil {
return nil, errors.New("err: response not found in etags cache")
}

return respBytes, nil
}

if resp.StatusCode >= 400 {
return nil, fmt.Errorf("err: response code %d", resp.StatusCode)
}

var bodyReader io.Reader = resp.Body

if resp.Header.Get("Content-Encoding") == "br" {
bodyReader = brotli.NewReader(bodyReader)
}

// Read the response body
respBytes, err := io.ReadAll(bodyReader)
if err != nil {
return nil, errors.New("err: failed to read response body")
}

// save etag and response to cache
etag = resp.Header.Get("ETag")
f.etags[id] = etag
f.measurements[etag] = respBytes

return respBytes, nil
}
Loading

0 comments on commit dea6d12

Please sign in to comment.