-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #117 from incident-io/lawrence-handle-client-errors
Handle validation errors for 204 endpoints
- Loading branch information
Showing
2 changed files
with
124 additions
and
50 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package client | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/deepmap/oapi-codegen/pkg/securityprovider" | ||
"github.com/hashicorp/go-retryablehttp" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
func New(ctx context.Context, apiKey, apiEndpoint, version string, opts ...ClientOption) (*ClientWithResponses, error) { | ||
bearerTokenProvider, bearerTokenProviderErr := securityprovider.NewSecurityProviderBearerToken(apiKey) | ||
if bearerTokenProviderErr != nil { | ||
return nil, bearerTokenProviderErr | ||
} | ||
|
||
retryClient := retryablehttp.NewClient() | ||
retryClient.RetryMax = maxRetries | ||
retryClient.Backoff = attentiveBackoff | ||
|
||
base := retryClient.StandardClient() | ||
|
||
// The generated client won't turn validation errors into actual errors, so we do this | ||
// inside of a generic middleware. | ||
base.Transport = Wrap(base.Transport, func(req *http.Request, next http.RoundTripper) (*http.Response, error) { | ||
resp, err := next.RoundTrip(req) | ||
if err != nil { | ||
return nil, err | ||
} | ||
if err == nil && resp.StatusCode > 299 { | ||
data, err := io.ReadAll(resp.Body) | ||
if err != nil { | ||
return nil, fmt.Errorf("status %d: no response body", resp.StatusCode) | ||
} | ||
|
||
return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(data)) | ||
} | ||
|
||
return resp, err | ||
}) | ||
|
||
clientOpts := append([]ClientOption{ | ||
WithHTTPClient(base), | ||
WithRequestEditorFn(bearerTokenProvider.Intercept), | ||
// Add a user-agent so we can tell which version these requests came from. | ||
WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { | ||
req.Header.Add("user-agent", fmt.Sprintf("terraform-provider-incident/%s", version)) | ||
return nil | ||
}), | ||
}, opts...) | ||
|
||
client, err := NewClientWithResponses(apiEndpoint, clientOpts...) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "creating client") | ||
} | ||
|
||
return client, nil | ||
} | ||
|
||
const ( | ||
maxRetries = 10 | ||
) | ||
|
||
func attentiveBackoff(minDuration, maxDuration time.Duration, attemptNum int, resp *http.Response) time.Duration { | ||
// Retry for rate limits and server errors. | ||
if resp != nil && resp.StatusCode == http.StatusTooManyRequests { | ||
// Check for a 'Retry-After' header. | ||
retryAfter := resp.Header.Get("Retry-After") | ||
if retryAfter != "" { | ||
retryAfterDate, err := time.Parse(time.RFC1123, retryAfter) | ||
if err != nil { | ||
// If we can't parse the Retry-After, lets just wait for 10 seconds | ||
return 10 | ||
} | ||
|
||
timeToWait := time.Until(retryAfterDate) | ||
|
||
if timeToWait < 1*time.Second { | ||
// by default lets back off at least 1 second | ||
return 1 * time.Second | ||
} | ||
|
||
return timeToWait | ||
} | ||
|
||
} | ||
// otherwise use the default backoff | ||
return retryablehttp.DefaultBackoff(minDuration, maxDuration, attemptNum, resp) | ||
} | ||
|
||
// WithReadOnly restricts the client to GET requests only, useful when creating a client | ||
// for the purpose of dry-running. | ||
func WithReadOnly() ClientOption { | ||
return WithRequestEditorFn(func(ctx context.Context, req *http.Request) error { | ||
if req.Method != http.MethodGet { | ||
return fmt.Errorf("read-only client tried to make mutating request: %s %s", req.Method, req.URL.String()) | ||
} | ||
|
||
return nil | ||
}) | ||
} | ||
|
||
// RoundTripperFunc wraps a function to implement the RoundTripper interface, allowing | ||
// easy wrapping of existing round-trippers. | ||
type RoundTripperFunc func(req *http.Request) (*http.Response, error) | ||
|
||
func (f RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) { | ||
return f(req) | ||
} | ||
|
||
// Wrap allows easy wrapping of an existing RoundTripper with a function that can | ||
// optionally call the original, or do its own thing. | ||
func Wrap(next http.RoundTripper, apply func(req *http.Request, next http.RoundTripper) (*http.Response, error)) http.RoundTripper { | ||
return RoundTripperFunc(func(req *http.Request) (*http.Response, error) { | ||
return apply(req, next) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters