140 lines
4.9 KiB
Go
140 lines
4.9 KiB
Go
|
|
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)
|
||
|
|
}
|