client/http_client.go (353 lines of code) (raw):
package client
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/hashicorp/go-retryablehttp"
)
// ErrNotFound for special cases instead of always returning http statusCode.
var ErrNotFound = errors.New("not found")
const requestsTimeoutSec = 30
type Client struct {
AppURL string
RestURL string
Token string
Username string
Password string
HTTPClient *http.Client
MaxRetries int
}
type Response struct {
StatusCode int
Body []byte
}
func NewClient(host, token, username, password string, maxRetries int) Client {
client := Client{
AppURL: host + "/app",
RestURL: host + "/app/rest",
Token: token,
Username: username,
Password: password,
HTTPClient: &http.Client{Timeout: requestsTimeoutSec * time.Second},
MaxRetries: maxRetries,
}
return client
}
// Deprecated: Use request instead. Deprecated since version v0.0.69
func (c *Client) doRequest(req *http.Request) ([]byte, error) {
return c.doRequestWithType(req, "application/json")
}
// Deprecated: Use requestWithType instead. Deprecated since version v0.0.69
func (c *Client) doRequestWithType(req *http.Request, ct string) ([]byte, error) {
if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token)
} else {
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(c.Username+":"+c.Password)))
}
req.Header.Set("Content-Type", ct)
req.Header.Set("Accept", "application/json, text/plain")
res, err := c.HTTPClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
return nil, fmt.Errorf("status: %d, body: %s", res.StatusCode, body)
}
return body, err
}
func (c *Client) request(req *http.Request) (Response, error) {
return c.requestWithType(req, "application/json")
}
func (c *Client) requestWithType(req *http.Request, ct string) (Response, error) {
c.setHeaders(req, ct)
res, err := c.HTTPClient.Do(req)
if err != nil {
return Response{}, fmt.Errorf("request failed: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return Response{}, fmt.Errorf("read response failed: %w", err)
}
if res.StatusCode == http.StatusNotFound {
return Response{
StatusCode: res.StatusCode,
Body: body,
},
nil
}
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
return Response{}, fmt.Errorf("status: %d, body: %s", res.StatusCode, body)
}
return Response{
StatusCode: res.StatusCode,
Body: body,
}, nil
}
// retryableRequest performs an HTTP request with retry logic using the provided retry policy and request object.
//
// Parameters:
// - req: The HTTP request to be executed with retry logic.
// - retryPolicy: A function defining the retry policy to be applied for the request.
//
// Returns:
// - Response: The response object containing the status code and body of the final request.
// - error: An error object if the request fails after retrying or other errors occur.
func (c *Client) retryableRequest(req *http.Request, retryPolicy retryablehttp.CheckRetry) (Response, error) {
return c.retryableRequestWithType(req, "application/json", retryPolicy)
}
func (c *Client) retryableRequestWithType(req *http.Request, ct string, retryPolicy retryablehttp.CheckRetry) (Response, error) {
c.setHeaders(req, ct)
rclient := retryablehttp.NewClient()
rclient.RetryWaitMin = 5 * time.Second
rclient.RetryWaitMax = 5 * time.Second
rclient.RetryMax = c.MaxRetries
rclient.CheckRetry = retryPolicy
// Convert http.Request to retryablehttp request
retryReq, err := retryablehttp.NewRequest(req.Method, req.URL.String(), req.Body)
retryReq.Header = req.Header
res, err := rclient.Do(retryReq)
if err != nil {
return Response{}, fmt.Errorf("request failed: %w", err)
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
return Response{}, fmt.Errorf("read response failed: %w", err)
}
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
return Response{}, fmt.Errorf("status: %d, body: %s", res.StatusCode, body)
}
return Response{
StatusCode: res.StatusCode,
Body: body,
}, nil
}
func (c *Client) GetField(resource, id, name string) (string, error) {
req, err := http.NewRequest(
"GET",
fmt.Sprintf("%s/%s/%s/%s", c.RestURL, resource, id, name),
nil,
)
if err != nil {
return "", err
}
body, err := c.doRequestWithType(req, "text/plain")
if err != nil {
return "", err
}
return string(body), nil
}
func (c *Client) SetField(resource, id, name string, value *string) (string, error) {
var method, body string
if value == nil {
method = "DELETE"
body = ""
} else {
method = "PUT"
body = *value
}
req, err := http.NewRequest(
method,
fmt.Sprintf("%s/%s/id:%s/%s", c.RestURL, resource, id, name),
strings.NewReader(body),
)
if err != nil {
return "", err
}
result, err := c.retryableRequestWithType(req, "text/plain", retryPolicy)
if err != nil {
return "", err
}
return string(result.Body), nil
}
func (c *Client) SetFieldJson(resource, id, name string, value interface{}) (string, error) {
var method string
var body []byte
var err error
if value == nil {
method = "DELETE"
body = make([]byte, 0)
} else {
method = "PUT"
body, err = json.Marshal(value)
if err != nil {
return "", err
}
}
req, err := http.NewRequest(
method,
fmt.Sprintf("%s/%s/id:%s/%s", c.RestURL, resource, id, name),
bytes.NewReader(body),
)
if err != nil {
return "", err
}
result, err := c.requestWithType(req, "application/json")
if err != nil {
return "", err
}
return string(result.Body), nil
}
// Verify authethication and status of the REST endpoint
func (c *Client) VerifyConnection(ctx context.Context) (Response, error) {
addr, err := c.verifyRequestAddr("")
if err != nil {
return Response{}, err
}
// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr.String(), nil)
if err != nil {
return Response{}, err
}
// Run text/plain request
response, err := c.requestWithType(req, "text/plain")
if err != nil {
return Response{}, err
}
// Verify response: defense against a non caught error when calling requestWithType
if response.StatusCode == http.StatusUnauthorized || response.StatusCode == http.StatusForbidden {
return response, fmt.Errorf("Got status %d when trying connection to the server", response.StatusCode)
}
return response, nil
}
// Uses default Background context. Use GetRequestWithContext for custom context. resp must be ready for json.Unmarshall
func (c *Client) GetRequest(endpoint, query string, resp any) error {
ctx := context.Background()
return c.GetRequestWithContext(ctx, endpoint, query, resp)
}
// Calling http methods directly. resp must be ready for json.Unmarshall
func (c *Client) GetRequestWithContext(ctx context.Context, endpoint, query string, resp any) error {
addr, err := c.verifyRequestAddr(endpoint)
if err != nil {
return err
}
// Adding queries
_, err = url.ParseQuery(query)
if err != nil {
return err
}
addr.RawQuery = query
// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr.String(), nil)
if err != nil {
return err
}
// Run request
response, err := c.request(req)
if err != nil {
return err
}
if response.StatusCode == http.StatusNotFound {
return ErrNotFound
}
// Unmarshal the response
err = json.Unmarshal(response.Body, resp)
if err != nil {
return err
}
return nil
}
// Uses default Background context. Use GetTextRequestWithContext for custom context. Returns body as string.
func (c *Client) GetTextRequest(endpoint, query string) (string, error) {
ctx := context.Background()
return c.GetTextRequestWithContext(ctx, endpoint, query)
}
// Calling http methods directly for text/plain endpoints. Returns body as string.
func (c *Client) GetTextRequestWithContext(ctx context.Context, endpoint, query string) (string, error) {
addr, err := c.verifyRequestAddr(endpoint)
if err != nil {
return "", err
}
// Adding queries
_, err = url.ParseQuery(query)
if err != nil {
return "", err
}
addr.RawQuery = query
// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodGet, addr.String(), nil)
if err != nil {
return "", err
}
// Run request as text/plain
response, err := c.requestWithType(req, "text/plain")
if err != nil {
return "", err
}
if response.StatusCode == http.StatusNotFound {
return "", ErrNotFound
}
return string(response.Body), nil
}
// Uses default Background context. Use PostRequestWithContext for custom context. resp must be ready for json.Unmarshall
func (c *Client) PostRequest(endpoint string, body io.Reader, resp any) error {
ctx := context.Background()
return c.PostRequestWithContext(ctx, endpoint, body, resp)
}
// Calling http methods directly. resp must be ready for json.Unmarshall if the post request returns body
func (c *Client) PostRequestWithContext(ctx context.Context, endpoint string, body io.Reader, resp any) error {
addr, err := c.verifyRequestAddr(endpoint)
if err != nil {
return err
}
// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodPost, addr.String(), body)
if err != nil {
return err
}
// Run request
response, err := c.request(req)
if err != nil {
return err
}
// Unmarshal the response if it is not empty
if len(response.Body) != 0 {
err = json.Unmarshal(response.Body, resp)
if err != nil {
return err
}
}
return nil
}
// Uses default Background context. Use DeleteRequestWithContext for custom context. resp must be ready for json.Unmarshall
func (c *Client) DeleteRequest(endpoint string) error {
ctx := context.Background()
return c.DeleteRequestWithContext(ctx, endpoint)
}
// Calling http methods directly
func (c *Client) DeleteRequestWithContext(ctx context.Context, endpoint string) error {
addr, err := c.verifyRequestAddr(endpoint)
if err != nil {
return err
}
// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, addr.String(), nil)
if err != nil {
return err
}
// Run request
_, err = c.request(req)
if err != nil {
return err
}
return nil
}
// Uses default Background context. Use PutRequestWithContext for custom context. resp must be ready for json.Unmarshall
func (c *Client) PutRequest(endpoint string, body io.Reader, resp any) error {
ctx := context.Background()
return c.PutRequestWithContext(ctx, endpoint, body, resp)
}
// Calling http methods directly. resp must be ready for json.Unmarshall if the put request returns body
func (c *Client) PutRequestWithContext(ctx context.Context, endpoint string, body io.Reader, resp any) error {
addr, err := c.verifyRequestAddr(endpoint)
if err != nil {
return err
}
// Create request
req, err := http.NewRequestWithContext(ctx, http.MethodPut, addr.String(), body)
if err != nil {
return err
}
// Run request
response, err := c.request(req)
if err != nil {
return err
}
// Unmarshal the response if it is not empty
if len(response.Body) != 0 {
err = json.Unmarshal(response.Body, resp)
if err != nil {
return err
}
}
return nil
}
func (c *Client) verifyRequestAddr(endpoint string) (*url.URL, error) {
// Build full address and verify it
addr, err := url.Parse(c.RestURL)
if err != nil {
return nil, err
}
addr = addr.JoinPath(endpoint)
return addr, nil
}
func (c *Client) setHeaders(req *http.Request, ct string) {
if c.Token != "" {
req.Header.Set("Authorization", "Bearer "+c.Token)
} else {
req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(c.Username+":"+c.Password)))
}
req.Header.Set("Content-Type", ct)
req.Header.Set("Accept", ct)
}