1package gsclient 2 3import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "errors" 8 "fmt" 9 "io/ioutil" 10 "net" 11 "net/http" 12 "strconv" 13 "time" 14) 15 16//gsRequest gridscale's custom gsRequest struct 17type gsRequest struct { 18 uri string 19 method string 20 body interface{} 21 skipCheckingRequest bool 22} 23 24//CreateResponse common struct of a response for creation 25type CreateResponse struct { 26 //UUID of the object being created 27 ObjectUUID string `json:"object_uuid"` 28 29 //UUID of the request 30 RequestUUID string `json:"request_uuid"` 31} 32 33//RequestStatus status of a request 34type RequestStatus map[string]RequestStatusProperties 35 36//RequestStatusProperties JSON struct of properties of a request's status 37type RequestStatusProperties struct { 38 Status string `json:"status"` 39 Message string `json:"message"` 40 CreateTime GSTime `json:"create_time"` 41} 42 43//RequestError error of a request 44type RequestError struct { 45 Title string `json:"title"` 46 Description string `json:"description"` 47 StatusCode int 48 RequestUUID string 49} 50 51//Error just returns error as string 52func (r RequestError) Error() string { 53 message := r.Description 54 if message == "" { 55 message = "no error message received from server" 56 } 57 errorMessageFormat := "Status code: %v. Error: %s. Request UUID: %s. " 58 if r.StatusCode >= 500 { 59 errorMessageFormat += "Please report this error along with the request UUID." 60 } 61 return fmt.Sprintf(errorMessageFormat, r.StatusCode, message, r.RequestUUID) 62} 63 64const ( 65 requestUUIDHeaderParam = "X-Request-Id" 66 requestRateLimitResetHeaderParam = "ratelimit-reset" 67) 68 69//This function takes the client and a struct and then adds the result to the given struct if possible 70func (r *gsRequest) execute(ctx context.Context, c Client, output interface{}) error { 71 72 //Prepare http request (including HTTP headers preparation, etc.) 73 httpReq, err := r.prepareHTTPRequest(ctx, c.cfg) 74 logger.Debugf("Request body: %v", httpReq.Body) 75 logger.Debugf("Request headers: %v", httpReq.Header) 76 77 //Execute the request (including retrying when needed) 78 requestUUID, responseBodyBytes, err := r.retryHTTPRequest(ctx, c.HttpClient(), httpReq, c.MaxNumberOfRetries(), c.DelayInterval()) 79 if err != nil { 80 return err 81 } 82 83 //if output is set 84 if output != nil { 85 //Unmarshal body bytes to the given struct 86 err = json.Unmarshal(responseBodyBytes, output) 87 if err != nil { 88 logger.Errorf("Error while marshaling JSON: %v", err) 89 return err 90 } 91 } 92 93 //If the client is synchronous, and the request does not skip 94 //checking a request, wait until the request completes 95 if c.Synchronous() && !r.skipCheckingRequest { 96 return c.waitForRequestCompleted(ctx, requestUUID) 97 } 98 return nil 99} 100 101//prepareHTTPRequest prepares a http request 102func (r *gsRequest) prepareHTTPRequest(ctx context.Context, cfg *Config) (*http.Request, error) { 103 url := cfg.apiURL + r.uri 104 logger.Debugf("Preparing %v request sent to URL: %v", r.method, url) 105 106 //Convert the body of the request to json 107 jsonBody := new(bytes.Buffer) 108 if r.body != nil { 109 err := json.NewEncoder(jsonBody).Encode(r.body) 110 if err != nil { 111 return nil, err 112 } 113 } 114 115 //Add authentication headers and content type 116 request, err := http.NewRequest(r.method, url, jsonBody) 117 if err != nil { 118 return nil, err 119 } 120 request = request.WithContext(ctx) 121 request.Header.Set("User-Agent", cfg.userAgent) 122 request.Header.Set("X-Auth-UserID", cfg.userUUID) 123 request.Header.Set("X-Auth-Token", cfg.apiToken) 124 request.Header.Set("Content-Type", bodyType) 125 126 //Set headers based on a given list of custom headers 127 //Use Header.Set() instead of Header.Add() because we want to 128 //override the headers' values if they are already set. 129 for k, v := range cfg.httpHeaders { 130 request.Header.Set(k, v) 131 } 132 133 return request, nil 134} 135 136//retryHTTPRequest runs & retries a HTTP request 137//returns UUID (string), response body ([]byte), error 138func (r *gsRequest) retryHTTPRequest( 139 ctx context.Context, 140 httpClient *http.Client, 141 httpReq *http.Request, 142 maxNoOfRetries int, 143 delayInterval time.Duration, 144) (string, []byte, error) { 145 //Init request UUID variable 146 var requestUUID string 147 //Init empty response body 148 var responseBodyBytes []byte 149 // 150 err := retryWithLimitedNumOfRetries(func() (bool, error) { 151 //execute the request 152 resp, err := httpClient.Do(httpReq) 153 if err != nil { 154 //If the error is caused by expired context, return context error and no need to retry 155 if ctx.Err() != nil { 156 return false, ctx.Err() 157 } 158 159 if err, ok := err.(net.Error); ok { 160 // exclude retry request with none GET method (write operations) in case of a request timeout or a context error 161 if err.Timeout() && r.method != http.MethodGet { 162 return false, err 163 } 164 logger.Debugf("Retrying request due to network error %v", err) 165 return true, err 166 } 167 logger.Errorf("Error while executing the request: %v", err) 168 //stop retrying (false) and return error 169 return false, err 170 } 171 //Close body to prevent resource leak 172 defer resp.Body.Close() 173 174 statusCode := resp.StatusCode 175 requestUUID = resp.Header.Get(requestUUIDHeaderParam) 176 responseBodyBytes, err = ioutil.ReadAll(resp.Body) 177 if err != nil { 178 logger.Errorf("Error while reading the response's body: %v", err) 179 //stop retrying (false) and return error 180 return false, err 181 } 182 183 logger.Debugf("Status code: %v. Request UUID: %v. Headers: %v", statusCode, requestUUID, resp.Header) 184 185 //If the status code is an error code 186 if statusCode >= 300 { 187 var errorMessage RequestError //error messages have a different structure, so they are read with a different struct 188 errorMessage.StatusCode = statusCode 189 errorMessage.RequestUUID = requestUUID 190 json.Unmarshal(responseBodyBytes, &errorMessage) 191 192 //If the error is retryable 193 if isErrorHTTPCodeRetryable(statusCode) { 194 //If status code is 429, that means we reach the rate limit 195 if statusCode == 429 { 196 //Get the time that the rate limit will be reset 197 rateLimitResetTimestamp := resp.Header.Get(requestRateLimitResetHeaderParam) 198 //Get the delay time 199 delayMs, err := getDelayTimeInMsFromTimestampStr(rateLimitResetTimestamp) 200 if err != nil { 201 return false, err 202 } 203 //Delay the retry until the rate limit is reset 204 logger.Debugf("Delay request for %d ms: %v method sent to url %v with body %v", delayMs, r.method, httpReq.URL.RequestURI(), r.body) 205 time.Sleep(time.Duration(delayMs) * time.Millisecond) 206 } 207 logger.Debugf("Retrying request: %v method sent to url %v with body %v", r.method, httpReq.URL.RequestURI(), r.body) 208 return true, errorMessage 209 } 210 211 //If the error is not retryable 212 logger.Errorf( 213 "Error message: %v. Title: %v. Code: %v. Request UUID: %v.", 214 errorMessage.Description, 215 errorMessage.Title, 216 errorMessage.StatusCode, 217 errorMessage.RequestUUID, 218 ) 219 //stop retrying (false) and return custom error 220 return false, errorMessage 221 222 } 223 logger.Debugf("Response body: %v", string(responseBodyBytes)) 224 //stop retrying (false) as no more errors 225 return false, nil 226 }, maxNoOfRetries, delayInterval) 227 //No need to return when the context is already expired. 228 select { 229 case <-ctx.Done(): 230 return "", nil, ctx.Err() 231 default: 232 } 233 return requestUUID, responseBodyBytes, err 234} 235 236func isErrorHTTPCodeRetryable(statusCode int) bool { 237 //if internal server error (>=500) 238 //OR object is in status that does not allow the request (424) 239 //OR we reach the rate limit (429), retry 240 if statusCode >= 500 || statusCode == 424 || statusCode == 429 { 241 return true 242 } 243 //stop retrying (false) and return custom error 244 return false 245} 246 247//getDelayTimeInMsFromTimestampStr takes a unix timestamp (ms) string 248//and returns the amount of ms to delay the next HTTP request retry 249//return error if the input string is not a valid unix timestamp(ms) 250func getDelayTimeInMsFromTimestampStr(timestamp string) (int64, error) { 251 if timestamp == "" { 252 return 0, errors.New("timestamp is empty") 253 } 254 //convert timestamp from string to int 255 timestampInt, err := strconv.ParseInt(timestamp, 10, 64) 256 if err != nil { 257 return 0, err 258 } 259 currentTimestampMs := time.Now().UnixNano() / 1000000 260 return timestampInt - currentTimestampMs, nil 261} 262