Files
cybertron/pkg/httpClient/httpClient.go

140 lines
4.9 KiB
Go
Raw Normal View History

package httpclient
import (
"bytes"
"context"
"cybertron/configs"
"cybertron/pkg/log"
"cybertron/pkg/metrics"
"cybertron/pkg/utils"
"errors"
"fmt"
"go.uber.org/zap"
"google.golang.org/protobuf/proto"
"net/http"
"net/url"
"path"
"time"
)
type HttpClient interface {
Do(req *http.Request) (*http.Response, error)
}
type CircuitBreaker interface {
ExecuteRequest(req func() (interface{}, error)) (interface{}, error)
}
var logger = log.Log
// todo - custom configs for all clients
func NewHttpClient(httpConfig configs.HttpConfig) HttpClient {
return &http.Client{
Transport: &http.Transport{
MaxIdleConns: httpConfig.MaxIdleConnectionPool,
MaxConnsPerHost: httpConfig.MaxConnection,
},
Timeout: time.Duration(httpConfig.MaxTimeoutInSeconds * time.Second.Nanoseconds()),
}
}
// GetHttpRequest is a generic function to create a http request with the given method, url and body.
// Accepts the body as a proto message. Adds the necessary headers.
func GetHttpRequest(requestMetadata *context.Context, method string, url string, body proto.Message) (*http.Request, error) {
requestBody, _ := proto.Marshal(body)
req, err := http.NewRequest(method, url, bytes.NewBuffer(requestBody))
if err != nil {
return nil, err
}
err = addHeadersToRequest(req, requestMetadata)
if err != nil {
return nil, err
}
return req, nil
}
// addHeadersToRequest adds the necessary headers to the request.
func addHeadersToRequest(req *http.Request, requestMetadata *context.Context) error {
//correlationId := metadata.GetRequestMetadata(requestMetadata, utils.CORRELATION_ID_HEADER)
//if correlationId == nil {
// return fmt.Errorf("correlation id not found in request metadata")
//}
//req.Header.Add(utils.CORRELATION_ID_HEADER, *correlationId)
//
//saCustomerId := metadata.GetRequestMetadata(requestMetadata, utils.CUSTOMER_ID_HEADER)
//if saCustomerId != nil {
// req.Header.Add(utils.CUSTOMER_ID_HEADER, *saCustomerId)
//}
return nil
}
// GenerateUrl builds a url for the given endpoint and query params. To append path params to the url,
// pass each part of the path as a parameter. For example, if the endpoint is /billers/{billerId}/bills,
// then pass "billers", "{billerId}" and "bills" as parameters, in the same order.
func GenerateUrl(baseUrl, apiVersion string, queryParams map[string]string, endpoint ...string) (*url.URL, error) {
url, err := url.Parse(baseUrl)
if err != nil {
logger.Error(err.Error())
return nil, err
}
url.Path = path.Join(url.Path, apiVersion, path.Join(endpoint...))
if queryParams != nil {
query := url.Query()
for key, value := range queryParams {
query.Add(key, value)
}
url.RawQuery = query.Encode()
}
return url, nil
}
// HttpCallToClientWithCB is a generic function to make an api call to client service with circuit breaker pattern.
// Returns error only when http response is nil. This is because, in case of internal failures, downstream services are expected
// to share the error details in the response body.
func HttpCallToClientWithCB(requestMetadata *context.Context, req *http.Request, httpClient *HttpClient, circuitBreaker CircuitBreaker) (*http.Response, error) {
resp, err := circuitBreaker.ExecuteRequest(func() (interface{}, error) {
return HttpCallToClient(req, httpClient)
})
if resp == nil {
return nil, err
}
if httpResponse, ok := resp.(*http.Response); ok && httpResponse != nil {
if utils.IsErrorStatusCode(httpResponse.StatusCode) {
logger.ErrorWithCtx(requestMetadata, "api call failed", zap.String("url", req.URL.String()), zap.Int("status_code", httpResponse.StatusCode))
if httpResponse.Body == nil {
return nil, errors.New("api call failed: response body is nil")
}
} else {
logger.InfoWithCtx(requestMetadata, "api call successful", zap.String("url", req.URL.String()), zap.Int("status_code", httpResponse.StatusCode))
}
return httpResponse, nil
}
log.Log.ErrorWithCtx(requestMetadata, "unexpected response", zap.String("response", utils.ConvertToString(resp)))
return nil, errors.New("unexpected response type")
}
// HttpCallToClient is a generic function to make an api call to client service. It expects
// all the headers to be set in the request. Also records the metrics for the api call.
// In error scenario, downstream service is expected to return 4xx or 5xx status code with error details in the response body.
// Errors returned by this method will increase the failure counter in circuit breaker. Error is only returned if status code is 5XX.
func HttpCallToClient(req *http.Request, httpClient *HttpClient) (*http.Response, error) {
httpMethodCall := func(req *http.Request) (*http.Response, error) {
resp, err := (*httpClient).Do(req)
if err != nil {
return nil, err
}
// Only returning error in cases we want CB to consider failure.
if resp.StatusCode >= 500 {
return resp, errors.New("api call failed with status code: " + fmt.Sprintf("%d", resp.StatusCode))
}
return resp, nil
}
return metrics.RecordClientHttpCallMetrics(req, httpMethodCall)
}