diff --git a/internal/config/http_client.go b/internal/config/http_client.go new file mode 100644 index 000000000..bd740d237 --- /dev/null +++ b/internal/config/http_client.go @@ -0,0 +1,28 @@ +package config + +import ( + "github.com/namsral/flag" +) + +type ( + HTTPClient struct { + BaseURL string + Timeout int + } +) + +var httpClient *HTTPClient + +func (c *HTTPClient) Validate() error { + return nil +} + +func (*HTTPClient) Init(prefix ...string) *HTTPClient { + if httpClient != nil { + return httpClient + } + httpClient = new(HTTPClient) + flag.StringVar(&httpClient.BaseURL, "http-client-base-url", "", "HTTP Client Base URL") + flag.IntVar(&httpClient.Timeout, "http-client-timeout", 5, "HTTP Client request timeout (seconds)") + return httpClient +} diff --git a/internal/http/client.go b/internal/http/client.go new file mode 100644 index 000000000..c36fab941 --- /dev/null +++ b/internal/http/client.go @@ -0,0 +1,166 @@ +package http + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httputil" + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/crusttech/crust/internal/config" +) + +type ( + Client struct { + Transport *http.Transport + Client *http.Client + + debugLevel DebugLevel + config *config.HTTPClient + } + + Request http.Request + + DebugLevel string +) + +const ( + INFO DebugLevel = "info" + FULL = "full" +) + +func New(flags *config.HTTPClient) (*Client, error) { + if err := flags.Validate(); err != nil { + return nil, errors.Wrap(err, "creating http client failed") + } + + timeout := time.Duration(flags.Timeout) * time.Second + + transport := &http.Transport{ + Dial: (&net.Dialer{ + Timeout: timeout, + }).Dial, + TLSHandshakeTimeout: timeout, + } + + client := &http.Client{ + Timeout: timeout, + Transport: transport, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + return &Client{ + Transport: transport, + Client: client, + config: flags, + }, nil +} + +func (c *Client) Debug(level DebugLevel) *Client { + c.debugLevel = level + return c +} + +func (c *Client) Get(url string) (*http.Request, error) { + return c.Request("GET", url, nil) +} + +func (c *Client) Post(url string, body interface{}) (*http.Request, error) { + return c.Request("POST", url, body) +} + +func (c *Client) Patch(url string, body interface{}) (*http.Request, error) { + return c.Request("PATCH", url, body) +} + +func (c *Client) Delete(url string) (*http.Request, error) { + return c.Request("DELETE", url, nil) +} + +func (c *Client) Request(method, url string, body interface{}) (*http.Request, error) { + if c.config.BaseURL != "" { + url = strings.TrimRight(c.config.BaseURL, "/") + "/" + strings.TrimLeft(url, "/") + } + + if c.debugLevel == "info" { + fmt.Println("HTTP >>>", method, url) + } + + request := func() (*http.Request, error) { + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return nil, err + } + return http.NewRequest(method, url, bytes.NewBuffer(b)) + } + return http.NewRequest(method, url, nil) + } + + req, err := request() + if err != nil { + return nil, errors.Wrap(err, "creating request failed") + } + req.Header.Add("Content-Type", "application/json") + return req, nil +} + +func (c *Client) Do(req *http.Request) (*http.Response, error) { + if c.debugLevel == FULL { + fmt.Println("HTTP >>> (request)") + b, err := httputil.DumpRequestOut(req, true) + if err != nil { + fmt.Println("HTTP >>> Error:", err) + } else { + if b != nil { + fmt.Println(strings.TrimSpace(string(b))) + } + } + fmt.Println("---") + } + + resp, err := c.Client.Do(req) + if c.debugLevel == FULL { + fmt.Println("HTTP <<< (response)") + if err != nil { + fmt.Println("HTTP <<< Error:", err) + } else { + + b, err := httputil.DumpResponse(resp, true) + if err != nil { + fmt.Println("HTTP <<< Error:", err) + } else { + if b != nil { + fmt.Println(string(b)) + } + } + } + fmt.Println("-----------------") + } + if err != nil { + if c.debugLevel == INFO { + fmt.Println("HTTP <<< Response error", err) + } + return nil, errors.Wrap(err, "request failed") + } + if c.debugLevel == INFO { + fmt.Println("HTTP <<< Response", resp.StatusCode) + } + return resp, nil +} + +func ToError(resp *http.Response) error { + body, err := ioutil.ReadAll(resp.Body) + if body == nil || err != nil { + return errors.Errorf("unexpected response (%d, %s)", resp.StatusCode, err) + } + return errors.New(string(body)) +} diff --git a/internal/http/client_test.go b/internal/http/client_test.go new file mode 100644 index 000000000..2a9697d7b --- /dev/null +++ b/internal/http/client_test.go @@ -0,0 +1,36 @@ +// +build external + +package http + +import ( + "testing" + + "github.com/crusttech/crust/internal/config" + "github.com/crusttech/crust/internal/test" +) + +func TestHTTPClient(t *testing.T) { + client, err := New(&config.HTTPClient{ + Timeout: 10, + }) + test.Assert(t, err == nil, "%+v", err) + client.Debug(FULL) + + req, err := client.Get("https://api.scene-si.org/fortune.php") + test.Assert(t, err == nil, "%+v", err) + + resp, err := client.Do(req) + test.Assert(t, err == nil, "%+v", err) + + err = func() error { + defer resp.Body.Close() + switch resp.StatusCode { + case 200: + return nil + default: + return ToError(resp) + } + }() + + test.Assert(t, err == nil, "Invalid response: %+v", err) +}