github.com/angryronald/go-kit@v0.0.0-20240505173814-ff2bd9c79dbf/generic/http/client/client.service.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io/ioutil"
     9  	"math/rand"
    10  	"net/http"
    11  	"net/url"
    12  	"time"
    13  
    14  	"moul.io/http2curl"
    15  
    16  	"github.com/afex/hystrix-go/hystrix"
    17  	"github.com/redis/go-redis/v9"
    18  	"github.com/sirupsen/logrus"
    19  
    20  	"github.com/angryronald/go-kit/constant"
    21  	"github.com/angryronald/go-kit/types"
    22  )
    23  
    24  // AuthorizationTypeStruct represents struct of Authorization Type
    25  type AuthorizationTypeStruct struct {
    26  	HeaderName      string
    27  	HeaderType      string
    28  	HeaderTypeValue string
    29  	Token           string
    30  }
    31  
    32  // AuthorizationType represents the enum for http authorization type
    33  type AuthorizationType AuthorizationTypeStruct
    34  
    35  // Enum value for http authorization type
    36  var (
    37  	Basic       = AuthorizationType(AuthorizationTypeStruct{HeaderName: "authorization", HeaderType: "basic", HeaderTypeValue: "basic "})
    38  	Bearer      = AuthorizationType(AuthorizationTypeStruct{HeaderName: "authorization", HeaderType: "bearer", HeaderTypeValue: "bearer "})
    39  	AccessToken = AuthorizationType(AuthorizationTypeStruct{HeaderName: "access-token", HeaderType: "auth0", HeaderTypeValue: ""})
    40  	Secret      = AuthorizationType(AuthorizationTypeStruct{HeaderName: "secret", HeaderType: "secret", HeaderTypeValue: ""})
    41  	APIKey      = AuthorizationType(AuthorizationTypeStruct{HeaderName: "api-key", HeaderType: "api-key", HeaderTypeValue: ""})
    42  )
    43  
    44  //
    45  // Private constants
    46  //
    47  
    48  const apiURL = "https://127.0.0.1:8080"
    49  const defaultHTTPTimeout = 80 * time.Second
    50  const maxNetworkRetriesDelay = 5000 * time.Millisecond
    51  const minNetworkRetriesDelay = 500 * time.Millisecond
    52  
    53  //
    54  // Private variables
    55  //
    56  
    57  var httpClient = &http.Client{Timeout: defaultHTTPTimeout}
    58  
    59  // HTTPClient represents the service http client
    60  type HTTPClient struct {
    61  	redisClient        *redis.Client
    62  	APIURL             string
    63  	HTTPClient         *http.Client
    64  	MaxNetworkRetries  int
    65  	UseNormalSleep     bool
    66  	AuthorizationTypes []AuthorizationType
    67  	ClientName         string
    68  	log                *logrus.Logger
    69  }
    70  
    71  func (c *HTTPClient) shouldRetry(err error, res *http.Response, retry int) bool {
    72  	if retry >= c.MaxNetworkRetries {
    73  		return false
    74  	}
    75  
    76  	if err != nil {
    77  		return true
    78  	}
    79  
    80  	return false
    81  }
    82  
    83  func (c *HTTPClient) sleepTime(numRetries int) time.Duration {
    84  	if c.UseNormalSleep {
    85  		return 0
    86  	}
    87  
    88  	// exponentially backoff by 2^numOfRetries
    89  	delay := minNetworkRetriesDelay + minNetworkRetriesDelay*time.Duration(1<<uint(numRetries))
    90  	if delay > maxNetworkRetriesDelay {
    91  		delay = maxNetworkRetriesDelay
    92  	}
    93  
    94  	// generate random jitter to prevent thundering herd problem
    95  	jitter := rand.Int63n(int64(delay / 4))
    96  	delay -= time.Duration(jitter)
    97  
    98  	if delay < minNetworkRetriesDelay {
    99  		delay = minNetworkRetriesDelay
   100  	}
   101  
   102  	return delay
   103  }
   104  
   105  // Do calls the api http request and parse the response into v
   106  func (c *HTTPClient) Do(req *http.Request) (string, *ResponseError) {
   107  	var res *http.Response
   108  	var err error
   109  
   110  	for retry := 0; ; {
   111  		res, err = c.HTTPClient.Do(req)
   112  
   113  		if !c.shouldRetry(err, res, retry) {
   114  			break
   115  		}
   116  
   117  		sleepDuration := c.sleepTime(retry)
   118  		retry++
   119  
   120  		time.Sleep(sleepDuration)
   121  	}
   122  	if err != nil {
   123  		return "", &ResponseError{
   124  			Code:       "Internal Server Error",
   125  			Message:    "Error while retry",
   126  			Error:      err,
   127  			StatusCode: 500,
   128  			Info:       fmt.Sprintf("Error when retrying to call [%s]", c.APIURL),
   129  		}
   130  	}
   131  	defer res.Body.Close()
   132  
   133  	resBody, err := ioutil.ReadAll(res.Body)
   134  	res.Body.Close()
   135  	if err != nil {
   136  		return "", &ResponseError{
   137  			Code:       fmt.Sprint(res.StatusCode),
   138  			Message:    "",
   139  			StatusCode: res.StatusCode,
   140  			Error:      err,
   141  			Info:       fmt.Sprintf("Error when retrying to call [%s]", c.APIURL),
   142  		}
   143  	}
   144  
   145  	errResponse := &ResponseError{
   146  		Code:       fmt.Sprint(res.StatusCode),
   147  		Message:    "",
   148  		StatusCode: res.StatusCode,
   149  		Error:      nil,
   150  		Info:       "",
   151  	}
   152  	if res.StatusCode < 200 || res.StatusCode >= 300 {
   153  		err = json.Unmarshal([]byte(string(resBody)), errResponse)
   154  		if err != nil {
   155  			errResponse.Error = err
   156  		}
   157  		if errResponse.Info != "" {
   158  			errResponse.Message = errResponse.Info
   159  		}
   160  		errResponse.Error = fmt.Errorf("error while calling %s: %v", req.URL.String(), errResponse.Message)
   161  
   162  		return "", errResponse
   163  	}
   164  
   165  	return string(resBody), errResponse
   166  }
   167  
   168  // CallClient do call client
   169  func (c *HTTPClient) CallClient(ctx context.Context, path string, method constant.HTTPMethod, request interface{}, result interface{}) *ResponseError {
   170  	var jsonData []byte
   171  	var err error
   172  	var response string
   173  	var errDo *ResponseError
   174  
   175  	if request != nil && request != "" {
   176  		jsonData, err = json.Marshal(request)
   177  		if err != nil {
   178  			return errDo.FromError(err)
   179  		}
   180  	}
   181  
   182  	urlPath, err := url.Parse(fmt.Sprintf("%s/%s", c.APIURL, path))
   183  	if err != nil {
   184  		return errDo.FromError(err)
   185  	}
   186  
   187  	req, err := http.NewRequest(string(method), urlPath.String(), bytes.NewBuffer(jsonData))
   188  	if err != nil {
   189  		return errDo.FromError(err)
   190  	}
   191  
   192  	for _, authorizationType := range c.AuthorizationTypes {
   193  		if authorizationType.HeaderType != "APIKey" {
   194  			req.Header.Add(authorizationType.HeaderName, fmt.Sprintf("%s%s", authorizationType.HeaderTypeValue, authorizationType.Token))
   195  		}
   196  	}
   197  	req.Header.Add("content-type", "application/json")
   198  
   199  	requestRaw := types.Metadata{}
   200  	if request != nil && request != "" {
   201  		err = json.Unmarshal(jsonData, &requestRaw)
   202  		if err != nil {
   203  			return errDo.FromError(err)
   204  		}
   205  	}
   206  
   207  	response, errDo = c.Do(req)
   208  
   209  	//TODO change logging to store in elastic search / hadoop
   210  	command, _ := http2curl.GetCurlCommand(req)
   211  	c.log.Debugf(`
   212  	[Calling %s] 
   213  	curl: %v
   214  	response: %v 
   215  	`, c.ClientName, command.String(), response)
   216  
   217  	if errDo != nil && (errDo.Error != nil || errDo.Message != "") {
   218  		return errDo
   219  	}
   220  
   221  	if response != "" && result != nil {
   222  		err = json.Unmarshal([]byte(response), result)
   223  		if err != nil {
   224  			return errDo.FromError(err)
   225  		}
   226  	}
   227  
   228  	return errDo
   229  }
   230  
   231  // CallClientWithCachingInRedis call client with caching in redis
   232  func (c *HTTPClient) CallClientWithCachingInRedis(ctx context.Context, durationInSecond int, path string, method constant.HTTPMethod, request interface{}, result interface{}) *ResponseError {
   233  	var jsonData []byte
   234  	var err error
   235  	var response string
   236  	var errDo *ResponseError
   237  
   238  	if request != nil && request != "" {
   239  		jsonData, err = json.Marshal(request)
   240  		if err != nil {
   241  			return errDo.FromError(err)
   242  		}
   243  	}
   244  
   245  	urlPath, err := url.Parse(fmt.Sprintf("%s/%s", c.APIURL, path))
   246  	if err != nil {
   247  		return errDo.FromError(err)
   248  	}
   249  
   250  	//collect from redis if already exist
   251  	val, errRedis := c.redisClient.Get(ctx, "apicaching:"+urlPath.String()).Result()
   252  	if errRedis != nil {
   253  		c.log.Debugf(`
   254  		======================================================================
   255  		Error Collecting Caching in "CallClientWithCachingInRedis":
   256  		"key": %s
   257  		Error: %v
   258  		======================================================================
   259  		`, "apicaching:"+urlPath.String(), errRedis)
   260  	}
   261  
   262  	if val != "" {
   263  		isSuccess := true
   264  		if errJSON := json.Unmarshal([]byte(val), &result); errJSON != nil {
   265  			c.log.Debugf(`
   266  			======================================================================
   267  			Error Collecting Caching in "CallClientWithCachingInRedis":
   268  			"key": %s,
   269  			Error: %v,
   270  			======================================================================
   271  			`, "apicaching:"+urlPath.String(), errJSON)
   272  			isSuccess = false
   273  		}
   274  		if isSuccess {
   275  			return nil
   276  		}
   277  	}
   278  
   279  	req, err := http.NewRequest(string(method), urlPath.String(), bytes.NewBuffer(jsonData))
   280  	if err != nil {
   281  		return errDo.FromError(err)
   282  	}
   283  
   284  	for _, authorizationType := range c.AuthorizationTypes {
   285  		if authorizationType.HeaderType != "APIKey" {
   286  			req.Header.Add(authorizationType.HeaderName, fmt.Sprintf("%s%s", authorizationType.HeaderTypeValue, authorizationType.Token))
   287  		}
   288  	}
   289  	req.Header.Add("content-type", "application/json")
   290  
   291  	response, errDo = c.Do(req)
   292  
   293  	//TODO change logging to store in elastic search / hadoop
   294  	command, _ := http2curl.GetCurlCommand(req)
   295  	c.log.Debugf(`
   296  	[Calling %s] 
   297  	curl: %v
   298  	response: %v 
   299  	`, c.ClientName, command.String(), response)
   300  
   301  	if errDo != nil && (errDo.Error != nil || errDo.Message != "") {
   302  		return errDo
   303  	}
   304  
   305  	if response != "" && result != nil {
   306  		err = json.Unmarshal([]byte(response), result)
   307  		if err != nil {
   308  			return errDo.FromError(err)
   309  		}
   310  
   311  		if errRedis = c.redisClient.Set(
   312  			ctx,
   313  			fmt.Sprintf("%s:%s", "apicaching", urlPath.String()),
   314  			response,
   315  			time.Second*time.Duration(durationInSecond),
   316  		).Err(); err != nil {
   317  			c.log.Debugf(`
   318  			======================================================================
   319  			Error Storing Caching in "CallClientWithCachingInRedis":
   320  			"key": %s,
   321  			Error: %v,
   322  			======================================================================
   323  			`, "apicaching:"+urlPath.String(), err)
   324  		}
   325  	}
   326  
   327  	return errDo
   328  }
   329  
   330  // CallClientWithCachingInRedisWithDifferentKey call client with caching in redis with different key
   331  func (c *HTTPClient) CallClientWithCachingInRedisWithDifferentKey(ctx context.Context, durationInSecond int, path string, pathToBeStoredAsKey string, method constant.HTTPMethod, request interface{}, result interface{}) *ResponseError {
   332  	var jsonData []byte
   333  	var err error
   334  	var response string
   335  	var errDo *ResponseError
   336  
   337  	if request != nil && request != "" {
   338  		jsonData, err = json.Marshal(request)
   339  		if err != nil {
   340  			return errDo.FromError(err)
   341  		}
   342  	}
   343  
   344  	urlPath, err := url.Parse(fmt.Sprintf("%s/%s", c.APIURL, path))
   345  	if err != nil {
   346  		return errDo.FromError(err)
   347  	}
   348  
   349  	urlPathToBeStored, err := url.Parse(fmt.Sprintf("%s/%s", c.APIURL, pathToBeStoredAsKey))
   350  	if err != nil {
   351  		return errDo.FromError(err)
   352  	}
   353  
   354  	//collect from redis if already exist
   355  	val, errRedis := c.redisClient.Get(ctx, "apicaching:"+urlPathToBeStored.String()).Result()
   356  	if errRedis != nil {
   357  		c.log.Debugf(`
   358  		======================================================================
   359  		Error Collecting Caching in "CallClientWithCachingInRedisWithDifferentKey":
   360  		"key": %s
   361  		Error: %v
   362  		======================================================================
   363  		`, "apicaching:"+urlPathToBeStored.String(), errRedis)
   364  	}
   365  
   366  	if val != "" {
   367  		isSuccess := true
   368  		if errJSON := json.Unmarshal([]byte(val), &result); errJSON != nil {
   369  			c.log.Debugf(`
   370  			======================================================================
   371  			Error Collecting Caching in "CallClientWithCachingInRedisWithDifferentKey":
   372  			"key": %s,
   373  			Error: %v,
   374  			======================================================================
   375  			`, "apicaching:"+urlPathToBeStored.String(), errJSON)
   376  			isSuccess = false
   377  		}
   378  		if isSuccess {
   379  			return nil
   380  		}
   381  	}
   382  
   383  	req, err := http.NewRequest(string(method), urlPath.String(), bytes.NewBuffer(jsonData))
   384  	if err != nil {
   385  		return errDo.FromError(err)
   386  	}
   387  
   388  	for _, authorizationType := range c.AuthorizationTypes {
   389  		if authorizationType.HeaderType != "APIKey" {
   390  			req.Header.Add(authorizationType.HeaderName, fmt.Sprintf("%s%s", authorizationType.HeaderTypeValue, authorizationType.Token))
   391  		}
   392  	}
   393  	req.Header.Add("content-type", "application/json")
   394  
   395  	response, errDo = c.Do(req)
   396  
   397  	//TODO change logging to store in elastic search / hadoop
   398  	command, _ := http2curl.GetCurlCommand(req)
   399  	c.log.Debugf(`
   400  	[Calling %s] 
   401  	curl: %v
   402  	response: %v 
   403  	`, c.ClientName, command.String(), response)
   404  
   405  	if errDo != nil && (errDo.Error != nil || errDo.Message != "") {
   406  		return errDo
   407  	}
   408  
   409  	if response != "" && result != nil {
   410  		err = json.Unmarshal([]byte(response), result)
   411  		if err != nil {
   412  			return errDo.FromError(err)
   413  		}
   414  
   415  		if errRedis = c.redisClient.Set(
   416  			ctx,
   417  			fmt.Sprintf("%s:%s", "apicaching", urlPathToBeStored.String()),
   418  			response,
   419  			time.Second*time.Duration(durationInSecond),
   420  		).Err(); err != nil {
   421  			c.log.Debugf(`
   422  			======================================================================
   423  			Error Storing Caching in "CallClientWithCachingInRedisWithDifferentKey":
   424  			"key": %s,
   425  			Error: %v,
   426  			======================================================================
   427  			`, "apicaching:"+urlPathToBeStored.String(), err)
   428  		}
   429  	}
   430  
   431  	return errDo
   432  }
   433  
   434  // CallClientWithCircuitBreaker do call client with circuit breaker (async)
   435  func (c *HTTPClient) CallClientWithCircuitBreaker(ctx context.Context, path string, method constant.HTTPMethod, request interface{}, result interface{}) *ResponseError {
   436  	var jsonData []byte
   437  	var err error
   438  	var response string
   439  	var errDo *ResponseError
   440  
   441  	Sethystrix(c.ClientName)
   442  	err = hystrix.Do(c.ClientName, func() error {
   443  		if request != nil {
   444  			jsonData, err = json.Marshal(request)
   445  			if err != nil {
   446  				errDo.FromError(err)
   447  				return errDo.Error
   448  			}
   449  		}
   450  
   451  		urlPath, err := url.Parse(fmt.Sprintf("%s/%s", c.APIURL, path))
   452  		if err != nil {
   453  			errDo.FromError(err)
   454  			return errDo.Error
   455  		}
   456  
   457  		req, err := http.NewRequest(string(method), urlPath.String(), bytes.NewBuffer(jsonData))
   458  		if err != nil {
   459  			errDo.FromError(err)
   460  			return errDo.Error
   461  		}
   462  
   463  		for _, authorizationType := range c.AuthorizationTypes {
   464  			if authorizationType.HeaderType != "APIKey" {
   465  				req.Header.Add(authorizationType.HeaderName, fmt.Sprintf("%s%s", authorizationType.HeaderTypeValue, authorizationType.Token))
   466  			}
   467  		}
   468  		req.Header.Add("content-type", "application/json")
   469  
   470  		response, errDo = c.Do(req)
   471  
   472  		//TODO change logging to store in elastic search / hadoop
   473  		command, _ := http2curl.GetCurlCommand(req)
   474  		c.log.Debugf(`
   475  	[Calling %s] 
   476  	curl: %v
   477  	response: %v 
   478  	`, c.ClientName, command.String(), response)
   479  
   480  		if errDo != nil && (errDo.Error != nil || errDo.Message != "") {
   481  			return errDo.Error
   482  		}
   483  
   484  		if response != "" && result != nil {
   485  			err = json.Unmarshal([]byte(response), result)
   486  			if err != nil {
   487  				errDo = &ResponseError{
   488  					Error: err,
   489  				}
   490  				return errDo.Error
   491  			}
   492  		}
   493  		return nil
   494  	}, nil)
   495  
   496  	return errDo
   497  }
   498  
   499  // CallClientWithBaseURLGiven do call client with base url given
   500  func (c *HTTPClient) CallClientWithBaseURLGiven(ctx context.Context, url string, method constant.HTTPMethod, request interface{}, result interface{}) *ResponseError {
   501  	var jsonData []byte
   502  	var err error
   503  	var response string
   504  	var errDo *ResponseError
   505  
   506  	if request != nil && request != "" {
   507  		jsonData, err = json.Marshal(request)
   508  		if err != nil {
   509  			return errDo.FromError(err)
   510  		}
   511  	}
   512  
   513  	req, err := http.NewRequest(string(method), url, bytes.NewBuffer(jsonData))
   514  	if err != nil {
   515  		return errDo.FromError(err)
   516  	}
   517  
   518  	for _, authorizationType := range c.AuthorizationTypes {
   519  		if authorizationType.HeaderType != "APIKey" {
   520  			req.Header.Add(authorizationType.HeaderName, fmt.Sprintf("%s%s", authorizationType.HeaderTypeValue, authorizationType.Token))
   521  		}
   522  	}
   523  	req.Header.Add("content-type", "application/json")
   524  
   525  	response, errDo = c.Do(req)
   526  
   527  	//TODO change logging to store in elastic search / hadoop
   528  	command, _ := http2curl.GetCurlCommand(req)
   529  	c.log.Debugf(`
   530  	[Calling %s] 
   531  	curl: %v
   532  	response: %v 
   533  	`, c.ClientName, command.String(), response)
   534  
   535  	if errDo != nil && (errDo.Error != nil || errDo.Message != "") {
   536  		return errDo
   537  	}
   538  
   539  	if response != "" && result != nil {
   540  		err = json.Unmarshal([]byte(response), result)
   541  		if err != nil {
   542  			return errDo.FromError(err)
   543  		}
   544  	}
   545  
   546  	return errDo
   547  }
   548  
   549  // CallClientWithRequestInBytes do call client with request in bytes and omit acknowledge process - specific case for consumer
   550  func (c *HTTPClient) CallClientWithRequestInBytes(ctx context.Context, path string, method constant.HTTPMethod, request []byte, result interface{}) *ResponseError {
   551  	var jsonData []byte = request
   552  	var err error
   553  	var response string
   554  	var errDo *ResponseError
   555  
   556  	urlPath, err := url.Parse(fmt.Sprintf("%s/%s", c.APIURL, path))
   557  	if err != nil {
   558  		return errDo.FromError(err)
   559  	}
   560  
   561  	req, err := http.NewRequest(string(method), urlPath.String(), bytes.NewBuffer(jsonData))
   562  	if err != nil {
   563  		return errDo.FromError(err)
   564  	}
   565  
   566  	for _, authorizationType := range c.AuthorizationTypes {
   567  		if authorizationType.HeaderType != "APIKey" {
   568  			req.Header.Add(authorizationType.HeaderName, fmt.Sprintf("%s%s", authorizationType.HeaderTypeValue, authorizationType.Token))
   569  		}
   570  	}
   571  	req.Header.Add("content-type", "application/json")
   572  
   573  	response, errDo = c.Do(req)
   574  
   575  	//TODO change logging to store in elastic search / hadoop
   576  	command, _ := http2curl.GetCurlCommand(req)
   577  	c.log.Debugf(`
   578  	[Calling %s] 
   579  	curl: %v
   580  	response: %v 
   581  	`, c.ClientName, command.String(), response)
   582  
   583  	if errDo != nil && (errDo.Error != nil || errDo.Message != "") {
   584  		return errDo
   585  	}
   586  
   587  	if response != "" && result != nil {
   588  		err = json.Unmarshal([]byte(response), result)
   589  		if err != nil {
   590  			return errDo.FromError(err)
   591  		}
   592  	}
   593  
   594  	return errDo
   595  }
   596  
   597  // AddAuthentication do add authentication
   598  func (c *HTTPClient) AddAuthentication(ctx context.Context, authorizationType AuthorizationType) {
   599  	isExist := false
   600  	for key, singleAuthorizationType := range c.AuthorizationTypes {
   601  		if singleAuthorizationType.HeaderType == authorizationType.HeaderType {
   602  			c.AuthorizationTypes[key].Token = authorizationType.Token
   603  			isExist = true
   604  			break
   605  		}
   606  	}
   607  
   608  	if isExist == false {
   609  		c.AuthorizationTypes = append(c.AuthorizationTypes, authorizationType)
   610  	}
   611  }
   612  
   613  // NewHTTPClient creates the new http client
   614  func NewHTTPClient(
   615  	config HTTPClient,
   616  	redisClient *redis.Client,
   617  	log *logrus.Logger,
   618  ) GenericHTTPClient {
   619  	if config.HTTPClient == nil {
   620  		config.HTTPClient = httpClient
   621  	}
   622  
   623  	if config.APIURL == "" {
   624  		config.APIURL = apiURL
   625  	}
   626  
   627  	return &HTTPClient{
   628  		APIURL:             config.APIURL,
   629  		HTTPClient:         config.HTTPClient,
   630  		MaxNetworkRetries:  config.MaxNetworkRetries,
   631  		UseNormalSleep:     config.UseNormalSleep,
   632  		AuthorizationTypes: config.AuthorizationTypes,
   633  		ClientName:         config.ClientName,
   634  		redisClient:        redisClient,
   635  		log:                log,
   636  	}
   637  }
   638  
   639  // Sethystrix setting for client
   640  func Sethystrix(nameClient string) {
   641  	hystrix.ConfigureCommand(nameClient, hystrix.CommandConfig{
   642  		Timeout:               5000,
   643  		MaxConcurrentRequests: 100,
   644  		ErrorPercentThreshold: 20,
   645  	})
   646  }