Tags: #go #http
// report indicates the failing test and the host it is associated with.
func report(host, msg string, err error) {
log.Printf("%s, %s: %v\n", host, msg, err)
}
// TestStatusCode verifies the given status code will return a corresponding
// custom error page. If a status code that has no custom error page is
// provided, then we'll allow for an override to verify that a default custom
// error page was returned.
//
// We allow for an optional variadic parameter to be provided so a caller can
// specify a default status code to verify against in case the given status
// code is expected to return a different status code.
func TestStatusCode(statusCode int, host, needle string, config *settings.Config, defaultStatusCode ...int) {
client := &http.Client{
Timeout: time.Second * time.Duration(config.Timeout),
}
path := fmt.Sprintf("/httpbin/status/%d", statusCode)
method := "GET"
uuid := time.Now().UnixNano()
url := fmt.Sprintf("https://%s%s?cachebust=%d", host, path, uuid)
req, err := http.NewRequest(method, url, nil)
if err != nil {
msg := fmt.Sprintf("test%d: failed to create new request", statusCode)
report(host, msg, err)
return
}
if config.Environment == "stage" {
req.SetBasicAuth(config.BasicAuthUser, config.BasicAuthPass)
// not all services use the same stage auth credentials
if creds, ok := config.BasicAuthOverride[host]; ok {
req.SetBasicAuth(creds["user"], creds["pass"])
}
}
// set debug flag so we can inspect Surrogate-Control.
req.Header.Set("Fastly-Debug", "true")
// set header to allow request through to HTTPBin
req.Header.Set("X-Origin-HTTPBin", config.SecretHash)
// implement retry logic around our http request...
var resp *http.Response
{
attempts := 0
for attempts < 2 {
attempts++
resp, err = client.Do(req)
if err != nil {
if config.Debug {
msg := fmt.Sprintf("test%d: failed to make http request", statusCode)
report(host, msg, err)
}
continue
}
defer resp.Body.Close()
break
}
if attempts == 2 && resp == nil {
msg := fmt.Sprintf("test%d: failed to make http request after two attempts", statusCode)
report(host, msg, err)
return
}
}
// in case the provided status code doesn't have a custom error page of its
// own but uses a different status code, then enable the caller to provide
// the correct status code that was expected to be handled for that code.
//
// for example, 503 should really return a 500 as there is currently no
// custom error page for a 503 so we return the custom 500 instead.
statusToVerify := statusCode
if len(defaultStatusCode) == 1 {
statusToVerify = defaultStatusCode[0]
}
if resp.StatusCode != statusToVerify {
err := fmt.Errorf(resp.Status)
msg := fmt.Sprintf("test%d: unexpected response status code", statusCode)
report(host, msg, err)
log.Printf("%#v\n", resp)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
msg := fmt.Sprintf("test%d: failed to read response body", statusCode)
report(host, msg, err)
log.Printf("%#v\n", resp)
return
}
if !strings.Contains(string(body), needle) {
err := fmt.Errorf("missing error message in body")
msg := fmt.Sprintf("test%d: failed to load correct error page", statusCode)
report(host, msg, err)
log.Printf("%#v\n", resp)
return
}
if config.Debug {
msg := fmt.Sprintf("test%d: passed", statusCode)
log.Println(host, msg)
}
}
// report indicates the failing test and the host it is associated with.
func report(host, msg string, err error) {
log.Printf("%s, %s: %v\n", host, msg, err)
}
// requestWithRetry will attempt to make a HTTP request multiple times before
// failing the given test scenario
func requestWithRetry(client *http.Client,
req *http.Request,
resp *http.Response,
retries int,
debug bool,
msg, host string) (*http.Response, error) {
attempts := 0
var err error
for attempts < retries {
attempts++
resp, err = client.Do(req)
if debug {
if err != nil {
report(host, msg, err)
}
continue
}
break
}
if attempts == retries && resp == nil {
report(host, fmt.Sprintf("%s after two attempts", msg), err)
return resp, err
}
return resp, nil
}
// TestStatusCode verifies the given status code will return a corresponding
// custom error page. If a status code that has no custom error page is
// provided, then we'll allow for an override to verify that a default custom
// error page was returned.
//
// We allow for an optional variadic parameter to be provided so a caller can
// specify a default status code to verify against in case the given status
// code is expected to return a different status code.
func TestStatusCode(statusCode int, host, needle string, config *settings.Config, defaultStatusCode ...int) {
client := &http.Client{
Timeout: time.Second * time.Duration(config.Timeout),
}
path := fmt.Sprintf("/httpbin/status/%d", statusCode)
method := "GET"
uuid := time.Now().UnixNano()
url := fmt.Sprintf("https://%s%s?cachebust=%d", host, path, uuid)
req, err := http.NewRequest(method, url, nil)
if err != nil {
msg := fmt.Sprintf("test%d: failed to create new request", statusCode)
report(host, msg, err)
return
}
if config.Environment == "stage" {
req.SetBasicAuth(config.BasicAuthUser, config.BasicAuthPass)
// not all services use the same stage auth credentials
if creds, ok := config.BasicAuthOverride[host]; ok {
req.SetBasicAuth(creds["user"], creds["pass"])
}
}
// set debug flag so we can inspect Surrogate-Control.
req.Header.Set("Fastly-Debug", "true")
// set header to allow request through to HTTPBin
req.Header.Set("X-Origin-HTTPBin", config.SecretHash)
var resp *http.Response
msg := fmt.Sprintf("test%d: failed to make http request", statusCode)
resp, err = requestWithRetry(client, req, resp, config.Retries, config.Debug, msg, host)
if err != nil {
return
}
defer resp.Body.Close()
// in case the provided status code doesn't have a custom error page of its
// own but uses a different status code, then enable the caller to provide
// the correct status code that was expected to be handled for that code.
//
// for example, 503 should really return a 500 as there is currently no
// custom error page for a 503 so we return the custom 500 instead.
statusToVerify := statusCode
if len(defaultStatusCode) == 1 {
statusToVerify = defaultStatusCode[0]
}
if resp.StatusCode != statusToVerify {
err := fmt.Errorf(resp.Status)
msg := fmt.Sprintf("test%d: unexpected response status code", statusCode)
report(host, msg, err)
log.Printf("%#v\n", resp)
return
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
msg := fmt.Sprintf("test%d: failed to read response body", statusCode)
report(host, msg, err)
log.Printf("%#v\n", resp)
return
}
if !strings.Contains(string(body), needle) {
err := fmt.Errorf("missing error message in body")
msg := fmt.Sprintf("test%d: failed to load correct error page", statusCode)
report(host, msg, err)
log.Printf("%#v\n", resp)
return
}
if config.Debug {
msg := fmt.Sprintf("test%d: passed", statusCode)
log.Println(host, msg)
}
}
// retry accepts a function and retries it up to n times before returning an error
func retry(n int, metricWriter metrics.Writer, kind string, cb func() error) error {
var err error
if err = cb(); err == nil {
return nil
}
for i := 0; i < n; i++ {
metricWriter.Incr("api_gateway.retry", 1.0, metrics.Tag{Key: "kind", Value: kind}, metrics.Tag{Key: "error", Value: err.Error()})
// Full Jitter from https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
// backoff = random_between(0, baseBackoff * 2 ** attempt)
maxBackoff := float64(50*time.Millisecond) * math.Exp2(float64(i))
backoff := time.Duration(rand.Float64() * maxBackoff)
log.WithFields(log.Fields{"retry_kind": kind, "count": i, "backoff": backoff, "error": err}).Info("retrying, backing off.")
time.Sleep(backoff)
err = cb()
if err == nil {
return nil
}
}
return err
}