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 }