github.com/kaleido-io/firefly@v0.0.0-20210622132723-8b4b6aacb971/internal/restclient/ffresty.go (about) 1 // Copyright © 2021 Kaleido, Inc. 2 // 3 // SPDX-License-Identifier: Apache-2.0 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 package restclient 18 19 import ( 20 "context" 21 "encoding/base64" 22 "fmt" 23 "io/ioutil" 24 "net/http" 25 "strings" 26 "time" 27 28 "github.com/go-resty/resty/v2" 29 "github.com/kaleido-io/firefly/internal/config" 30 "github.com/kaleido-io/firefly/internal/i18n" 31 "github.com/kaleido-io/firefly/internal/log" 32 "github.com/kaleido-io/firefly/pkg/fftypes" 33 ) 34 35 type retryCtxKey struct{} 36 37 type retryCtx struct { 38 id string 39 start time.Time 40 attempts uint 41 } 42 43 // OnAfterResponse when using SetDoNotParseResponse(true) for streming binary replies, 44 // the caller should invoke ffrest.OnAfterResponse on the response manually. 45 // The middleware is disabled on this path :-( 46 // See: https://github.com/go-resty/resty/blob/d01e8d1bac5ba1fed0d9e03c4c47ca21e94a7e8e/client.go#L912-L948 47 func OnAfterResponse(c *resty.Client, resp *resty.Response) { 48 if c == nil || resp == nil { 49 return 50 } 51 rctx := resp.Request.Context() 52 rc := rctx.Value(retryCtxKey{}).(*retryCtx) 53 elapsed := float64(time.Since(rc.start)) / float64(time.Millisecond) 54 log.L(rctx).Infof("<== %s %s [%d] (%.2fms)", resp.Request.Method, resp.Request.URL, resp.StatusCode(), elapsed) 55 } 56 57 // New creates a new Resty client, using static configuration (from the config file) 58 // from a given nested prefix in the static configuration 59 // 60 // You can use the normal Resty builder pattern, to set per-instance configuration 61 // as required. 62 func New(ctx context.Context, staticConfig config.Prefix) *resty.Client { 63 64 var client *resty.Client 65 66 iHTTPClient := staticConfig.Get(HTTPCustomClient) 67 if iHTTPClient != nil { 68 if httpClient, ok := iHTTPClient.(*http.Client); ok { 69 client = resty.NewWithClient(httpClient) 70 } 71 } 72 if client == nil { 73 client = resty.New() 74 } 75 76 url := strings.TrimSuffix(staticConfig.GetString(HTTPConfigURL), "/") 77 if url != "" { 78 client.SetHostURL(url) 79 log.L(ctx).Debugf("Created REST client to %s", url) 80 } 81 82 client.SetTimeout(staticConfig.GetDuration(HTTPConfigRequestTimeout)) 83 84 client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error { 85 rctx := req.Context() 86 rc := rctx.Value(retryCtxKey{}) 87 if rc == nil { 88 // First attempt 89 r := &retryCtx{ 90 id: fftypes.ShortID(), 91 start: time.Now(), 92 } 93 rctx = context.WithValue(rctx, retryCtxKey{}, r) 94 // Create a request logger from the root logger passed into the client 95 l := log.L(ctx).WithField("breq", r.id) 96 rctx = log.WithLogger(rctx, l) 97 req.SetContext(rctx) 98 } 99 log.L(rctx).Infof("==> %s %s%s", req.Method, url, req.URL) 100 return nil 101 }) 102 103 // Note that callers using SetNotParseResponse will need to invoke this themselves 104 105 client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error { OnAfterResponse(c, r); return nil }) 106 107 headers := staticConfig.GetObject(HTTPConfigHeaders) 108 for k, v := range headers { 109 if vs, ok := v.(string); ok { 110 client.SetHeader(k, vs) 111 } 112 } 113 authUsername := staticConfig.GetString((HTTPConfigAuthUsername)) 114 authPassword := staticConfig.GetString((HTTPConfigAuthPassword)) 115 if authUsername != "" && authPassword != "" { 116 client.SetHeader("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", authUsername, authPassword))))) 117 } 118 119 if staticConfig.GetBool(HTTPConfigRetryEnabled) { 120 retryCount := staticConfig.GetInt(HTTPConfigRetryCount) 121 minTimeout := staticConfig.GetDuration(HTTPConfigRetryInitDelay) 122 maxTimeout := staticConfig.GetDuration(HTTPConfigRetryMaxDelay) 123 client. 124 SetRetryCount(retryCount). 125 SetRetryWaitTime(minTimeout). 126 SetRetryMaxWaitTime(maxTimeout). 127 AddRetryCondition(func(r *resty.Response, err error) bool { 128 if r == nil || r.IsSuccess() { 129 return false 130 } 131 rctx := r.Request.Context() 132 rc := rctx.Value(retryCtxKey{}).(*retryCtx) 133 log.L(rctx).Infof("retry %d/%d (min=%dms/max=%dms) status=%d", rc.attempts, retryCount, minTimeout.Milliseconds(), maxTimeout.Milliseconds(), r.StatusCode()) 134 rc.attempts++ 135 return true 136 }) 137 } 138 139 return client 140 } 141 142 func WrapRestErr(ctx context.Context, res *resty.Response, err error, key i18n.MessageKey) error { 143 var respData string 144 if res != nil { 145 if res.RawBody() != nil { 146 defer func() { _ = res.RawBody().Close() }() 147 if r, err := ioutil.ReadAll(res.RawBody()); err == nil { 148 respData = string(r) 149 } 150 } 151 if respData == "" { 152 respData = res.String() 153 } 154 if len(respData) > 256 { 155 respData = respData[0:256] + "..." 156 } 157 } 158 if err != nil { 159 return i18n.WrapError(ctx, err, key, respData) 160 } 161 return i18n.NewError(ctx, key, respData) 162 }