-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathbase_service.go
887 lines (767 loc) · 31.2 KB
/
base_service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
package core
// (C) Copyright IBM Corp. 2019, 2022.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httputil"
"net/url"
"reflect"
"regexp"
"strconv"
"strings"
"time"
cleanhttp "github.com/hashicorp/go-cleanhttp"
retryablehttp "github.com/hashicorp/go-retryablehttp"
)
const (
headerNameUserAgent = "User-Agent"
sdkName = "ibm-go-sdk-core"
maxRedirects = 10
)
// ServiceOptions is a struct of configuration values for a service.
type ServiceOptions struct {
// This is the base URL associated with the service instance. This value will
// be combined with the paths for each operation to form the request URL
// [required].
URL string
// Authenticator holds the authenticator implementation to be used by the
// service instance to authenticate outbound requests, typically by adding the
// HTTP "Authorization" header.
Authenticator Authenticator
// EnableGzipCompression indicates whether or not request bodies
// should be gzip-compressed.
// This field has no effect on response bodies.
// If enabled, the Body field will be gzip-compressed and
// the "Content-Encoding" header will be added to the request with the
// value "gzip".
EnableGzipCompression bool
}
// BaseService implements the common functionality shared by generated services
// to manage requests and responses, authenticate outbound requests, etc.
type BaseService struct {
// Configuration values for a service.
Options *ServiceOptions
// A set of "default" http headers to be included with each outbound request.
DefaultHeaders http.Header
// The HTTP Client used to send requests and receive responses.
Client *http.Client
// The value to be used for the "User-Agent" HTTP header that is added to each
// outbound request. If this value is not set, then a default value will be
// used for the header.
UserAgent string
}
// NewBaseService constructs a new instance of BaseService. Validation on input
// parameters and service options will be performed before instance creation.
func NewBaseService(options *ServiceOptions) (*BaseService, error) {
if HasBadFirstOrLastChar(options.URL) {
return nil, fmt.Errorf(ERRORMSG_PROP_INVALID, "URL")
}
if IsNil(options.Authenticator) {
return nil, fmt.Errorf(ERRORMSG_NO_AUTHENTICATOR)
}
if err := options.Authenticator.Validate(); err != nil {
return nil, err
}
service := BaseService{
Options: options,
Client: DefaultHTTPClient(),
}
// Set a default value for the User-Agent http header.
service.SetUserAgent(service.buildUserAgent())
return &service, nil
}
// Clone will return a copy of "service" suitable for use by a
// generated service instance to process requests.
func (service *BaseService) Clone() *BaseService {
if IsNil(service) {
return nil
}
// First, copy the service options struct.
serviceOptions := *service.Options
// Next, make a copy of the service struct, then use the copy of the service options.
// Note, we'll re-use the "Client" instance from the original BaseService instance.
clone := *service
clone.Options = &serviceOptions
return &clone
}
// ConfigureService updates the service with external configuration values.
func (service *BaseService) ConfigureService(serviceName string) error {
// Try to load service properties from external config.
serviceProps, err := getServiceProperties(serviceName)
if err != nil {
return err
}
// If we were able to load any properties for this service, then check to see if the
// service-level properties were present and set them on the service if so.
if serviceProps != nil {
// URL
if url, ok := serviceProps[PROPNAME_SVC_URL]; ok && url != "" {
err := service.SetURL(url)
if err != nil {
return err
}
}
// DISABLE_SSL
if disableSSL, ok := serviceProps[PROPNAME_SVC_DISABLE_SSL]; ok && disableSSL != "" {
// Convert the config string to bool.
boolValue, err := strconv.ParseBool(disableSSL)
if err != nil {
boolValue = false
}
// If requested, disable SSL.
if boolValue {
service.DisableSSLVerification()
}
}
// ENABLE_GZIP
if enableGzip, ok := serviceProps[PROPNAME_SVC_ENABLE_GZIP]; ok && enableGzip != "" {
// Convert the config string to bool.
boolValue, err := strconv.ParseBool(enableGzip)
if err == nil {
service.SetEnableGzipCompression(boolValue)
}
}
// ENABLE_RETRIES
// If "ENABLE_RETRIES" is set to true, then we'll also try to retrieve "MAX_RETRIES" and
// "RETRY_INTERVAL". If those are not specified, we'll use 0 to trigger a default value for each.
if enableRetries, ok := serviceProps[PROPNAME_SVC_ENABLE_RETRIES]; ok && enableRetries != "" {
boolValue, err := strconv.ParseBool(enableRetries)
if boolValue && err == nil {
var maxRetries int = 0
var retryInterval time.Duration = 0
var s string
var ok bool
if s, ok = serviceProps[PROPNAME_SVC_MAX_RETRIES]; ok && s != "" {
n, err := strconv.ParseInt(s, 10, 32)
if err == nil {
maxRetries = int(n)
}
}
if s, ok = serviceProps[PROPNAME_SVC_RETRY_INTERVAL]; ok && s != "" {
n, err := strconv.ParseInt(s, 10, 32)
if err == nil {
retryInterval = time.Duration(n) * time.Second
}
}
service.EnableRetries(maxRetries, retryInterval)
}
}
}
return nil
}
// SetURL sets the service URL.
//
// Deprecated: use SetServiceURL instead.
func (service *BaseService) SetURL(url string) error {
return service.SetServiceURL(url)
}
// SetServiceURL sets the service URL.
func (service *BaseService) SetServiceURL(url string) error {
if HasBadFirstOrLastChar(url) {
return fmt.Errorf(ERRORMSG_PROP_INVALID, "URL")
}
service.Options.URL = url
return nil
}
// GetServiceURL returns the service URL.
func (service *BaseService) GetServiceURL() string {
return service.Options.URL
}
// SetDefaultHeaders sets HTTP headers to be sent in every request.
func (service *BaseService) SetDefaultHeaders(headers http.Header) {
service.DefaultHeaders = headers
}
// SetHTTPClient will set "client" as the http.Client instance to be used
// to invoke individual HTTP requests.
// If automatic retries are currently enabled on "service", then
// "client" will be set as the embedded client instance within
// the retryable client; otherwise "client" will be stored
// directly on "service".
func (service *BaseService) SetHTTPClient(client *http.Client) {
setupHTTPClient(client)
if isRetryableClient(service.Client) {
// If "service" is currently holding a retryable client,
// then set "client" as the embedded client used for individual requests.
tr := service.Client.Transport.(*retryablehttp.RoundTripper)
tr.Client.HTTPClient = client
} else {
// Otherwise, just hang "client" directly off the base service.
service.Client = client
}
}
// GetHTTPClient will return the http.Client instance used
// to invoke individual HTTP requests.
// If automatic retries are enabled, the returned value will
// be the http.Client instance embedded within the retryable client.
// If automatic retries are not enabled, then the returned value
// will simply be the "Client" field of the base service.
func (service *BaseService) GetHTTPClient() *http.Client {
if isRetryableClient(service.Client) {
tr := service.Client.Transport.(*retryablehttp.RoundTripper)
return tr.Client.HTTPClient
}
return service.Client
}
// DisableSSLVerification will configure the service to
// skip the verification of server certificates and hostnames.
// This will make the client susceptible to "man-in-the-middle"
// attacks. This should be used only for testing or in secure
// environments.
func (service *BaseService) DisableSSLVerification() {
// Make sure we have a non-nil client hanging off the BaseService.
if service.Client == nil {
service.Client = DefaultHTTPClient()
}
client := service.GetHTTPClient()
if tr, ok := client.Transport.(*http.Transport); tr != nil && ok {
// If no TLS config, then create a new one.
if tr.TLSClientConfig == nil {
tr.TLSClientConfig = &tls.Config{} // #nosec G402
}
// Disable server ssl cert & hostname verification.
tr.TLSClientConfig.InsecureSkipVerify = true // #nosec G402
}
}
// IsSSLDisabled returns true if and only if the service's http.Client instance
// is configured to skip verification of server SSL certificates.
func (service *BaseService) IsSSLDisabled() bool {
client := service.GetHTTPClient()
if client != nil {
if tr, ok := client.Transport.(*http.Transport); tr != nil && ok {
if tr.TLSClientConfig != nil {
return tr.TLSClientConfig.InsecureSkipVerify
}
}
}
return false
}
// setupHTTPClient will configure "client" for use with the BaseService object.
func setupHTTPClient(client *http.Client) {
// Set our "CheckRedirect" function to allow safe headers to be included
// in redirected requests under certain conditions.
if client.CheckRedirect == nil {
client.CheckRedirect = checkRedirect
}
}
// checkRedirect is used as an override for the default "CheckRedirect" function supplied
// by the net/http package and implements some additional logic required by IBM SDKs.
func checkRedirect(req *http.Request, via []*http.Request) error {
// The net/http module is implemented such that it will only include "safe" headers
// ("Authorization", "WWW-Authenticate", "Cookie", "Cookie2") when redirecting a request
// if the redirected host is the same host or a sub-domain of the original request's host.
// Example: foo.com redirected to foo.com or bar.foo.com would work, but bar.com would not.
// This "CheckRedirect" implementation will propagate "safe" headers in a redirected request
// only in situations where the hosts associated with the original and redirected request URLs
// are both located within the ".cloud.ibm.com" domain.
// First, perform the check that is done by the default CheckRedirect function
// to ensure we don't exhaust our max redirect limit.
if len(via) >= maxRedirects {
GetLogger().Debug("Exceeded max redirects: %d", maxRedirects)
return fmt.Errorf("stopped after %d redirects", maxRedirects)
}
if len(via) > 0 {
GetLogger().Debug("Detected %d prior request(s)", len(via))
originalReq := via[0]
redirectedReq := req
GetLogger().Debug("Redirecting request from %s to %s", originalReq.URL.String(), redirectedReq.URL.String())
redirectedHeader := req.Header
originalHeader := via[0].Header
originalHost := originalReq.URL.Hostname()
redirectedHost := redirectedReq.URL.Hostname()
if shouldCopySafeHeadersOnRedirect(originalHost, redirectedHost) {
// We're only concerned with "safe" headers since these are the ones that are not
// propagated automatically by net/http for a "cross-site" redirect.
for _, headerKey := range []string{"Authorization", "WWW-Authenticate", "Cookie", "Cookie2"} {
// If the original request contains a value for "headerKey"
// *and* this header is not already present in the redirected request,
// then copy the value from the original request to the redirected request.
if v, inOriginalRequest := originalHeader[headerKey]; inOriginalRequest {
if _, inRedirectedRequest := redirectedHeader[headerKey]; !inRedirectedRequest {
redirectedHeader[headerKey] = v
GetLogger().Debug("Propagating header '%s' in redirected request", headerKey)
}
}
}
} else {
GetLogger().Debug("Redirected request is not within the trusted domain.")
}
} else {
GetLogger().Debug("Detected no prior requests!")
}
return nil
}
// shouldCopySafeHeadersOnRedirect returns true iff safe headers should be copied
// to a redirected request.
func shouldCopySafeHeadersOnRedirect(fromHost, toHost string) bool {
GetLogger().Debug("hosts: %s %s", fromHost, toHost)
sameHost := fromHost == toHost
safeDomain := strings.HasSuffix(fromHost, ".cloud.ibm.com") && strings.HasSuffix(toHost, ".cloud.ibm.com")
return sameHost || safeDomain
}
// SetEnableGzipCompression sets the service's EnableGzipCompression field
func (service *BaseService) SetEnableGzipCompression(enableGzip bool) {
service.Options.EnableGzipCompression = enableGzip
}
// GetEnableGzipCompression returns the service's EnableGzipCompression field
func (service *BaseService) GetEnableGzipCompression() bool {
return service.Options.EnableGzipCompression
}
// buildUserAgent builds the user agent string.
func (service *BaseService) buildUserAgent() string {
return fmt.Sprintf("%s-%s %s", sdkName, __VERSION__, SystemInfo())
}
// SetUserAgent sets the user agent value.
func (service *BaseService) SetUserAgent(userAgentString string) {
if userAgentString == "" {
userAgentString = service.buildUserAgent()
}
service.UserAgent = userAgentString
}
// Request invokes the specified HTTP request and returns the response.
//
// Parameters:
// req: the http.Request object that holds the request information
//
// result: a pointer to the operation result. This should be one of:
// - *io.ReadCloser (for a byte-stream type response)
// - *<primitive>, *[]<primitive>, *map[string]<primitive>
// - *map[string]json.RawMessage, *[]json.RawMessage
//
// Return values:
// detailedResponse: a DetailedResponse instance containing the status code, headers, etc.
//
// err: a non-nil error object if an error occurred
func (service *BaseService) Request(req *http.Request, result interface{}) (detailedResponse *DetailedResponse, err error) {
// Add default headers.
if service.DefaultHeaders != nil {
for k, v := range service.DefaultHeaders {
req.Header.Add(k, strings.Join(v, ""))
}
// After adding the default headers, make one final check to see if the user
// specified the "Host" header within the default headers.
// This needs to be handled separately because it will be ignored by
// the Request.Write() method.
host := service.DefaultHeaders.Get("Host")
if host != "" {
req.Host = host
}
}
// Add the default User-Agent header if not already present.
userAgent := req.Header.Get(headerNameUserAgent)
if userAgent == "" {
req.Header.Add(headerNameUserAgent, service.UserAgent)
}
// Add authentication to the outbound request.
if IsNil(service.Options.Authenticator) {
err = fmt.Errorf(ERRORMSG_NO_AUTHENTICATOR)
return
}
authError := service.Options.Authenticator.Authenticate(req)
if authError != nil {
err = fmt.Errorf(ERRORMSG_AUTHENTICATE_ERROR, authError.Error())
castErr, ok := authError.(*AuthenticationError)
if ok {
detailedResponse = castErr.Response
}
return
}
// If debug is enabled, then dump the request.
if GetLogger().IsLogLevelEnabled(LevelDebug) {
buf, dumpErr := httputil.DumpRequestOut(req, !IsNil(req.Body))
if dumpErr == nil {
GetLogger().Debug("Request:\n%s\n", RedactSecrets(string(buf)))
} else {
GetLogger().Debug("error while attempting to log outbound request: %s", dumpErr.Error())
}
}
// Invoke the request, then check for errors during the invocation.
var httpResponse *http.Response
httpResponse, err = service.Client.Do(req)
if err != nil {
if strings.Contains(err.Error(), SSL_CERTIFICATION_ERROR) {
err = fmt.Errorf(ERRORMSG_SSL_VERIFICATION_FAILED + "\n" + err.Error())
}
return
}
// If debug is enabled, then dump the response.
if GetLogger().IsLogLevelEnabled(LevelDebug) {
buf, dumpErr := httputil.DumpResponse(httpResponse, !IsNil(httpResponse.Body))
if err == nil {
GetLogger().Debug("Response:\n%s\n", RedactSecrets(string(buf)))
} else {
GetLogger().Debug("error while attempting to log inbound response: %s", dumpErr.Error())
}
}
// Start to populate the DetailedResponse.
detailedResponse = &DetailedResponse{
StatusCode: httpResponse.StatusCode,
Headers: httpResponse.Header,
}
contentType := httpResponse.Header.Get(CONTENT_TYPE)
// If the operation was unsuccessful, then set up the DetailedResponse
// and error objects appropriately.
if httpResponse.StatusCode < 200 || httpResponse.StatusCode >= 300 {
var responseBody []byte
// First, read the response body into a byte array.
if !IsNil(httpResponse.Body) {
var readErr error
defer httpResponse.Body.Close() // #nosec G307
responseBody, readErr = io.ReadAll(httpResponse.Body)
if readErr != nil {
err = fmt.Errorf(ERRORMSG_READ_RESPONSE_BODY, readErr.Error())
return
}
}
// If the responseBody is empty, then just return a generic error based on the status code.
if len(responseBody) == 0 {
err = fmt.Errorf(http.StatusText(httpResponse.StatusCode))
return
}
// For a JSON-based error response body, decode it into a map (generic JSON object).
if IsJSONMimeType(contentType) {
// Return the error response body as a map, along with an
// error object containing our best guess at an error message.
responseMap, decodeErr := decodeAsMap(responseBody)
if decodeErr == nil {
detailedResponse.Result = responseMap
err = fmt.Errorf(getErrorMessage(responseMap, detailedResponse.StatusCode))
return
}
}
// For a non-JSON response or if we tripped while decoding the JSON response,
// just return the response body byte array in the RawResult field along with
// an error object that contains the generic error message for the status code.
detailedResponse.RawResult = responseBody
err = fmt.Errorf(http.StatusText(httpResponse.StatusCode))
return
}
// Operation was successful and we are expecting a response, so process the response.
if !IsNil(result) {
resultType := reflect.TypeOf(result).String()
// If 'result' is a io.ReadCloser, then pass the response body back reflectively via 'result'
// and bypass any further unmarshalling of the response.
if resultType == "*io.ReadCloser" {
rResult := reflect.ValueOf(result).Elem()
rResult.Set(reflect.ValueOf(httpResponse.Body))
detailedResponse.Result = httpResponse.Body
} else {
// First, read the response body into a byte array.
defer httpResponse.Body.Close() // #nosec G307
responseBody, readErr := io.ReadAll(httpResponse.Body)
if readErr != nil {
err = fmt.Errorf(ERRORMSG_READ_RESPONSE_BODY, readErr.Error())
return
}
// If the response body is empty, then skip any attempt to deserialize and just return
if len(responseBody) == 0 {
return
}
// If the content-type indicates JSON, then unmarshal the response body as JSON.
if IsJSONMimeType(contentType) {
// Decode the byte array as JSON.
decodeErr := json.NewDecoder(bytes.NewReader(responseBody)).Decode(result)
if decodeErr != nil {
// Error decoding the response body.
// Return the response body in RawResult, along with an error.
err = fmt.Errorf(ERRORMSG_UNMARSHAL_RESPONSE_BODY, decodeErr.Error())
detailedResponse.RawResult = responseBody
return
}
// Decode step was successful. Return the decoded response object in the Result field.
detailedResponse.Result = reflect.ValueOf(result).Elem().Interface()
return
}
// Check to see if the caller wanted the response body as a string.
// If the caller passed in 'result' as the address of *string,
// then we'll reflectively set result to point to it.
if resultType == "**string" {
responseString := string(responseBody)
rResult := reflect.ValueOf(result).Elem()
rResult.Set(reflect.ValueOf(&responseString))
// And set the string in the Result field.
detailedResponse.Result = &responseString
} else if resultType == "*[]uint8" { // byte is an alias for uint8
rResult := reflect.ValueOf(result).Elem()
rResult.Set(reflect.ValueOf(responseBody))
// And set the byte slice in the Result field.
detailedResponse.Result = responseBody
} else {
// At this point, we don't know how to set the result field, so we have to return an error.
// But make sure we save the bytes we read in the DetailedResponse for debugging purposes
detailedResponse.Result = responseBody
err = fmt.Errorf(ERRORMSG_UNEXPECTED_RESPONSE, contentType, resultType)
return
}
}
} else if !IsNil(httpResponse.Body) {
// We weren't expecting a response, but we have a reponse body,
// so we need to close it now since we're not going to consume it.
_ = httpResponse.Body.Close()
}
return
}
// Errors is a struct used to hold an array of errors received in an operation
// response.
type Errors struct {
Errors []Error `json:"errors,omitempty"`
}
// Error is a struct used to represent a single error received in an operation
// response.
type Error struct {
Message string `json:"message,omitempty"`
}
// decodeAsMap: Decode the specified JSON byte-stream into a map (akin to a generic JSON object).
// Notes:
// 1. This function will return the map (result of decoding the byte-stream) as well as the raw
// byte buffer. We return the byte buffer in addition to the decoded map so that the caller can
// re-use (if necessary) the stream of bytes after we've consumed them via the JSON decode step.
// 2. The primary return value of this function will be:
// a) an instance of map[string]interface{} if the specified byte-stream was successfully
// decoded as JSON.
// b) the string form of the byte-stream if the byte-stream could not be successfully
// decoded as JSON.
// 3. This function will close the io.ReadCloser before returning.
func decodeAsMap(byteBuffer []byte) (result map[string]interface{}, err error) {
err = json.NewDecoder(bytes.NewReader(byteBuffer)).Decode(&result)
return
}
// getErrorMessage: try to retrieve an error message from the decoded response body (map).
func getErrorMessage(responseMap map[string]interface{}, statusCode int) string {
// If the response contained the "errors" field, then try to deserialize responseMap
// into an array of Error structs, then return the first entry's "Message" field.
if _, ok := responseMap["errors"]; ok {
var errors Errors
responseBuffer, _ := json.Marshal(responseMap)
if err := json.Unmarshal(responseBuffer, &errors); err == nil {
return errors.Errors[0].Message
}
}
// Return the "error" field if present and is a string.
if val, ok := responseMap["error"]; ok {
errorMsg, ok := val.(string)
if ok {
return errorMsg
}
}
// Return the "message" field if present and is a string.
if val, ok := responseMap["message"]; ok {
errorMsg, ok := val.(string)
if ok {
return errorMsg
}
}
// Finally, return the "errorMessage" field if present and is a string.
if val, ok := responseMap["errorMessage"]; ok {
errorMsg, ok := val.(string)
if ok {
return errorMsg
}
}
// If we couldn't find an error message above, just return the generic text
// for the status code.
return http.StatusText(statusCode)
}
// isRetryableClient() will return true if and only if "client" is
// an http.Client instance that is configured for automatic retries.
// A retryable client is a client whose transport is a
// retryablehttp.RoundTripper instance.
func isRetryableClient(client *http.Client) bool {
var isRetryable bool = false
if client != nil && client.Transport != nil {
_, isRetryable = client.Transport.(*retryablehttp.RoundTripper)
}
return isRetryable
}
// EnableRetries will configure the service to perform automatic retries of failed requests.
// If "maxRetries" and/or "maxRetryInterval" are specified as 0, then default values
// are used instead.
//
// In a scenario where retries ARE NOT enabled:
// - BaseService.Client will be a "normal" http.Client instance used to invoke requests
// - BaseService.Client.Transport will be an instance of the default http.RoundTripper
// - BaseService.Client.Do() calls http.RoundTripper.RoundTrip() to invoke the request
// - Only one http.Client instance needed/used (BaseService.Client) in this scenario
// - Result: "normal" request processing without any automatic retries being performed
//
// In a scenario where retries ARE enabled:
// - BaseService.Client will be a "shim" http.Client instance
// - BaseService.Client.Transport will be an instance of retryablehttp.RoundTripper
// - BaseService.Client.Do() calls retryablehttp.RoundTripper.RoundTrip() (via the shim)
// to invoke the request
// - The retryablehttp.RoundTripper instance is configured with the retryablehttp.Client
// instance which holds the various retry config properties (max retries, max interval, etc.)
// - The retryablehttp.RoundTripper.RoundTrip() method triggers the retry logic in the retryablehttp.Client
// - The retryablehttp.Client instance's HTTPClient field holds a "normal" http.Client instance,
// which is used to invoke individual requests within the retry loop.
// - To summarize, there are three client instances used for request processing in this scenario:
// 1. The "shim" http.Client instance (BaseService.Client)
// 2. The retryablehttp.Client instance that implements the retry logic
// 3. The "normal" http.Client instance embedded in the retryablehttp.Client which is used to invoke
// individual requests within the retry logic
// - Result: Each request is invoked such that the automatic retry logic is employed
func (service *BaseService) EnableRetries(maxRetries int, maxRetryInterval time.Duration) {
if isRetryableClient(service.Client) {
// If retries are already enabled, then we just need to adjust
// the retryable client's config using "maxRetries" and "maxRetryInterval".
tr := service.Client.Transport.(*retryablehttp.RoundTripper)
if maxRetries > 0 {
tr.Client.RetryMax = maxRetries
}
if maxRetryInterval > 0 {
tr.Client.RetryWaitMax = maxRetryInterval
}
} else {
// Otherwise, we need to create a new retryable client instance
// and hang it off the base service.
client := NewRetryableClientWithHTTPClient(service.Client)
if maxRetries > 0 {
client.RetryMax = maxRetries
}
if maxRetryInterval > 0 {
client.RetryWaitMax = maxRetryInterval
}
// Hang the retryable client off the base service via the "shim" client.
service.Client = client.StandardClient()
}
}
// DisableRetries will disable automatic retries in the service.
func (service *BaseService) DisableRetries() {
if isRetryableClient(service.Client) {
// If the current client hanging off the base service is retryable,
// then we need to get ahold of the embedded http.Client instance
// and set that on the base service and effectively remove
// the retryable client instance.
tr := service.Client.Transport.(*retryablehttp.RoundTripper)
service.Client = tr.Client.HTTPClient
}
}
// DefaultHTTPClient returns a non-retryable http client with default configuration.
func DefaultHTTPClient() *http.Client {
client := cleanhttp.DefaultPooledClient()
setupHTTPClient(client)
return client
}
// httpLogger is a shim layer used to allow the Go core's logger to be used with the retryablehttp interfaces.
type httpLogger struct {
}
func (l *httpLogger) Printf(format string, inserts ...interface{}) {
if GetLogger().IsLogLevelEnabled(LevelDebug) {
msg := fmt.Sprintf(format, inserts...)
GetLogger().Log(LevelDebug, RedactSecrets(msg))
}
}
// NewRetryableHTTPClient returns a new instance of a retryable client
// with a default configuration that supports Go SDK usage.
func NewRetryableHTTPClient() *retryablehttp.Client {
return NewRetryableClientWithHTTPClient(nil)
}
// NewRetryableClientWithHTTPClient will return a new instance of a
// retryable client, using "httpClient" as the embedded client used to
// invoke individual requests within the retry logic.
// If "httpClient" is passed in as nil, then a default HTTP client will be
// used as the embedded client instead.
func NewRetryableClientWithHTTPClient(httpClient *http.Client) *retryablehttp.Client {
client := retryablehttp.NewClient()
client.Logger = &httpLogger{}
client.CheckRetry = IBMCloudSDKRetryPolicy
client.Backoff = IBMCloudSDKBackoffPolicy
client.ErrorHandler = retryablehttp.PassthroughErrorHandler
if httpClient != nil {
// If a non-nil http client was passed in, then let's use that
// as our embedded client used to invoke individual requests.
client.HTTPClient = httpClient
} else {
// Otherwise, we'll construct a default HTTP client and use that
client.HTTPClient = DefaultHTTPClient()
}
return client
}
var (
// A regular expression to match the error returned by net/http when the
// configured number of redirects is exhausted. This error isn't typed
// specifically so we resort to matching on the error string.
redirectsErrorRe = regexp.MustCompile(`stopped after \d+ redirects\z`)
// A regular expression to match the error returned by net/http when the
// scheme specified in the URL is invalid. This error isn't typed
// specifically so we resort to matching on the error string.
schemeErrorRe = regexp.MustCompile(`unsupported protocol scheme`)
)
// IBMCloudSDKRetryPolicy provides a default implementation of the CheckRetry interface
// associated with a retryablehttp.Client.
// This function will return true if the specified request/response should be retried.
func IBMCloudSDKRetryPolicy(ctx context.Context, resp *http.Response, err error) (bool, error) {
// This logic was adapted from go-relyablehttp.ErrorPropagatedRetryPolicy().
// Do not retry on a Context-related error (Canceled or DeadlineExceeded).
if ctx.Err() != nil {
return false, ctx.Err()
}
// Next, check for a few non-retryable errors.
if err != nil {
if v, ok := err.(*url.Error); ok {
// Don't retry if the error was due to too many redirects.
if redirectsErrorRe.MatchString(v.Error()) {
return false, v
}
// Don't retry if the error was due to an invalid protocol scheme.
if schemeErrorRe.MatchString(v.Error()) {
return false, v
}
// Don't retry if the error was due to TLS cert verification failure.
if _, ok := v.Err.(x509.UnknownAuthorityError); ok {
return false, v
}
}
// The error is likely recoverable so retry.
return true, nil
}
// Now check the status code.
// A 429 should be retryable.
// All codes in the 500's range except for 501 (Not Implemented) should be retryable.
if resp.StatusCode == 429 || (resp.StatusCode >= 500 && resp.StatusCode <= 599 && resp.StatusCode != 501) {
return true, nil
}
return false, nil
}
// IBMCloudSDKBackoffPolicy provides a default implementation of the Backoff interface
// associated with a retryablehttp.Client.
// This function will return the wait time to be associated with the next retry attempt.
func IBMCloudSDKBackoffPolicy(min, max time.Duration, attemptNum int, resp *http.Response) time.Duration {
// Check for a Retry-After header.
if resp != nil {
if s, ok := resp.Header["Retry-After"]; ok {
// First, try to parse the value as an integer (number of seconds to wait)
if sleep, err := strconv.ParseInt(s[0], 10, 64); err == nil {
return time.Second * time.Duration(sleep)
}
// Otherwise, try to parse the value as an HTTP Time value.
if retryTime, err := http.ParseTime(s[0]); err == nil {
sleep := time.Until(retryTime)
if sleep > max {
sleep = max
}
return sleep
}
}
}
// If no header-based wait time can be determined, then ask DefaultBackoff()
// to compute an exponential backoff.
return retryablehttp.DefaultBackoff(min, max, attemptNum, resp)
}