InVisionApp/go-health

View on GitHub
checkers/http.go

Summary

Maintainability
A
0 mins
Test Coverage
package checkers

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "io/ioutil"
    "net/http"
    "net/url"
    "strings"
    "time"
)

const (
    defaultHTTPTimeout = time.Duration(3) * time.Second
)

// HTTPConfig is used for configuring an HTTP check. The only required field is `URL`.
//
// "Method" is optional and defaults to `GET` if undefined.
//
// "Payload" is optional and can accept `string`, `[]byte` or will attempt to
// marshal the input to JSON for use w/ `bytes.NewReader()`.
//
// "StatusCode" is optional and defaults to `200`.
//
// "Expect" is optional; if defined, operates as a basic "body should contain <string>".
//
// "Client" is optional; if undefined, a new client will be created using "Timeout".
//
// "Timeout" is optional and defaults to "3s".
type HTTPConfig struct {
    URL        *url.URL      // Required
    Method     string        // Optional (default GET)
    Payload    interface{}   // Optional
    StatusCode int           // Optional (default 200)
    Expect     string        // Optional
    Client     *http.Client  // Optional
    Timeout    time.Duration // Optional (default 3s)
}

// HTTP implements the "ICheckable" interface.
type HTTP struct {
    Config *HTTPConfig
}

// NewHTTP creates a new HTTP checker that can be used for ".AddCheck(s)".
func NewHTTP(cfg *HTTPConfig) (*HTTP, error) {
    if cfg == nil {
        return nil, fmt.Errorf("Passed in config cannot be nil")
    }

    if err := cfg.prepare(); err != nil {
        return nil, fmt.Errorf("Unable to prepare given config: %v", err)
    }

    return &HTTP{
        Config: cfg,
    }, nil
}

// Status is used for performing an HTTP check against a dependency; it satisfies
// the "ICheckable" interface.
func (h *HTTP) Status() (interface{}, error) {
    resp, err := h.do()
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    // Check if StatusCode matches
    if resp.StatusCode != h.Config.StatusCode {
        return nil, fmt.Errorf("Received status code '%v' does not match expected status code '%v'",
            resp.StatusCode, h.Config.StatusCode)
    }

    // If Expect is set, verify if returned response contains expected data
    if h.Config.Expect != "" {
        data, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            return nil, fmt.Errorf("Unable to read response body to perform content expectancy check: %v", err)
        }

        if !strings.Contains(string(data), h.Config.Expect) {
            return nil, fmt.Errorf("Received response body '%v' does not contain expected content '%v'",
                string(data), h.Config.Expect)
        }
    }

    return nil, nil
}

func (h *HTTP) do() (*http.Response, error) {
    payload, err := parsePayload(h.Config.Payload)
    if err != nil {
        return nil, fmt.Errorf("error parsing payload: %v", err)
    }

    req, err := http.NewRequest(h.Config.Method, h.Config.URL.String(), payload)
    if err != nil {
        return nil, fmt.Errorf("Unable to create new HTTP request for HTTPMonitor check: %v", err)
    }

    resp, err := h.Config.Client.Do(req)
    if err != nil {
        return nil, fmt.Errorf("Ran into error while performing '%v' request: %v", h.Config.Method, err)
    }

    return resp, nil
}

func (h *HTTPConfig) prepare() error {
    if h.URL == nil {
        return errors.New("URL cannot be nil")
    }

    // Default StatusCode to 200
    if h.StatusCode == 0 {
        h.StatusCode = http.StatusOK
    }

    // Default to GET
    if h.Method == "" {
        h.Method = "GET"
    }

    if h.Timeout == 0 {
        h.Timeout = defaultHTTPTimeout
    }

    if h.Client == nil {
        h.Client = &http.Client{Timeout: h.Timeout}
    } else {
        h.Client.Timeout = h.Timeout
    }

    return nil
}

func parsePayload(b interface{}) (io.Reader, error) {
    if b == nil {
        return nil, nil
    }

    switch b.(type) {
    case []byte:
        return bytes.NewReader(b.([]byte)), nil
    case string:
        return bytes.NewReader([]byte(b.(string))), nil
    default:
        jb, err := json.Marshal(b)
        if err != nil {
            return nil, fmt.Errorf("failed to marshal json body: %v", err)
        }

        return bytes.NewReader(jb), nil
    }
}