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) }