Skip to content

Commit

Permalink
- adds doc comments for retry and redirect handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
baywet committed Nov 1, 2021
1 parent 2360279 commit 5032c16
Show file tree
Hide file tree
Showing 6 changed files with 94 additions and 46 deletions.
2 changes: 1 addition & 1 deletion http/go/nethttp/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import nethttp "net/http"

// Middleware interface for cross cutting concerns with HTTP requests and responses.
type Middleware interface {
// intercepts the request and returns the resposne. The implementer MUST call pipeline.Next()
// intercepts the request and returns the response. The implementer MUST call pipeline.Next()
// Parameters:
// - the pipeline to be executed after the middleware
// - the request to be processed
Expand Down
23 changes: 21 additions & 2 deletions http/go/nethttp/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,32 @@ package nethttplibrary

import nethttp "net/http"

// Pipeline contract for middleware infrastructure
type Pipeline interface {
// Next moves the request object through middlewares in the pipeline
// Parameters:
// req: the request object
// Returns:
// the response object
// error: any error that occurred
Next(req *nethttp.Request) (*nethttp.Response, error)
}

// custom transport for net/http with a middleware pipeline
type customTransport struct {
nethttp.Transport
// middleware pipeline in use for the client
middlewarePipeline *middlewarePipeline
}

// middleware pipeline implementation using a roundtripper from net/http
type middlewarePipeline struct {
// index of the middleware beeing executed
middlewareIndex int
transport nethttp.RoundTripper
middlewares []Middleware
// the round tripper to use to execute the request
transport nethttp.RoundTripper
// the middlewares to execute
middlewares []Middleware
}

func newMiddlewarePipeline(middlewares []Middleware) *middlewarePipeline {
Expand Down Expand Up @@ -44,6 +58,11 @@ func (transport *customTransport) RoundTrip(req *nethttp.Request) (*nethttp.Resp
return transport.middlewarePipeline.Next(req)
}

// Creates a new custom transport for http client with the provided set of middleware
// Parameters:
// middlewares: the middlewares to use
// Returns:
// the custom transport
func NewCustomTransport(middlewares ...Middleware) *customTransport {
if len(middlewares) == 0 {
middlewares = GetDefaultMiddlewares()
Expand Down
52 changes: 32 additions & 20 deletions http/go/nethttp/redirect_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,35 @@ import (
abs "github.com/microsoft/kiota/abstractions/go"
)

// The redirect handler handles redirect responses and follows them according to the options specified.
type RedirectHandler struct {
// options to use when evaluating whether to redirect or not
options RedirectHandlerOptions
}

// Creates a new redirect handler with the default options.
func NewRedirectHandler() *RedirectHandler {
return NewRedirectHandlerWithOptions(RedirectHandlerOptions{
MaxRedirects: DEFAULT_MAX_REDIRECTS,
MaxRedirects: defaultMaxRedirects,
ShouldRedirect: func(req *nethttp.Request, res *nethttp.Response) bool {
return true
},
})
}

// Creates a new redirect handler with the specified options.
// Parameters:
// options - the options to use when evaluating whether to redirect or not
func NewRedirectHandlerWithOptions(options RedirectHandlerOptions) *RedirectHandler {
return &RedirectHandler{options: options}
}

// Options to use when evaluating whether to redirect or not.
type RedirectHandlerOptions struct {
// A callback that determines whether to redirect or not.
ShouldRedirect func(req *nethttp.Request, res *nethttp.Response) bool
MaxRedirects int
// The maximum number of redirects to follow.
MaxRedirects int
}

var redirectKeyValue = abs.RequestOptionKey{
Expand All @@ -40,30 +50,35 @@ type redirectHandlerOptionsInt interface {
GetMaxRedirect() int
}

// Returns the key value to be used when the option is added to the request context
func (o *RedirectHandlerOptions) GetKey() abs.RequestOptionKey {
return redirectKeyValue
}

// Returns the redirection evaluation function.
func (o *RedirectHandlerOptions) GetShouldRedirect() func(req *nethttp.Request, res *nethttp.Response) bool {
return o.ShouldRedirect
}

// Returns the maximum number of redirects to follow.
func (o *RedirectHandlerOptions) GetMaxRedirect() int {
if o == nil || o.MaxRedirects < 1 {
return DEFAULT_MAX_REDIRECTS
} else if o.MaxRedirects > ABSOLUTE_MAX_REDIRECTS {
return ABSOLUTE_MAX_REDIRECTS
return defaultMaxRedirects
} else if o.MaxRedirects > absoluteMaxRedirects {
return absoluteMaxRedirects
} else {
return o.MaxRedirects
}
}

var DEFAULT_MAX_REDIRECTS = 5
var ABSOLUTE_MAX_REDIRECTS = 20
var MOVED_PERMANENTLY = 301
var FOUND = 302
var SEE_OTHER = 303
var TEMPORARY_REDIRECT = 307
var PERMANENT_REDIRECT = 308
var LOCATION_HEADER = "Location"
const defaultMaxRedirects = 5
const absoluteMaxRedirects = 20
const movedPermanently = 301
const found = 302
const seeOther = 303
const temporaryRedirect = 307
const permanentRedirect = 308
const locationHeader = "Location"

func (middleware RedirectHandler) Intercept(pipeline Pipeline, req *nethttp.Request) (*nethttp.Response, error) {
response, err := pipeline.Next(req)
Expand Down Expand Up @@ -100,22 +115,19 @@ func (middleware RedirectHandler) isRedirectResponse(response *nethttp.Response)
if response == nil {
return false
}
locationHeader := response.Header.Get(LOCATION_HEADER)
locationHeader := response.Header.Get(locationHeader)
if locationHeader == "" {
return false
}
statusCode := response.StatusCode
return statusCode == MOVED_PERMANENTLY || statusCode == FOUND || statusCode == SEE_OTHER || statusCode == TEMPORARY_REDIRECT || statusCode == PERMANENT_REDIRECT
return statusCode == movedPermanently || statusCode == found || statusCode == seeOther || statusCode == temporaryRedirect || statusCode == permanentRedirect
}

func (middleware RedirectHandler) getRedirectRequest(request *nethttp.Request, response *nethttp.Response) (*nethttp.Request, error) {
if request == nil || response == nil {
return nil, errors.New("request or response is nil")
}
locationHeaderValue := response.Header.Get(LOCATION_HEADER)
if locationHeaderValue == "" {
return nil, errors.New("location header is empty")
}
locationHeaderValue := response.Header.Get(locationHeader)
if locationHeaderValue[0] == '/' {
locationHeaderValue = request.URL.Scheme + "://" + request.URL.Host + locationHeaderValue
}
Expand All @@ -130,7 +142,7 @@ func (middleware RedirectHandler) getRedirectRequest(request *nethttp.Request, r
if !sameHost || !sameScheme {
result.Header.Del("Authorization")
}
if response.StatusCode == SEE_OTHER {
if response.StatusCode == seeOther {
result.Method = nethttp.MethodGet
result.Header.Del("Content-Type")
result.Header.Del("Content-Length")
Expand Down
2 changes: 1 addition & 1 deletion http/go/nethttp/redirect_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestItHonoursMaxRedirect(t *testing.T) {
t.Error(err)
}
assert.NotNil(t, resp)
assert.Equal(t, int64(DEFAULT_MAX_REDIRECTS+1), requestCount)
assert.Equal(t, int64(defaultMaxRedirects+1), requestCount)
}

func TestItStripsAuthorizationHeaderOnDifferentHost(t *testing.T) {
Expand Down
59 changes: 38 additions & 21 deletions http/go/nethttp/retry_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import (
abs "github.com/microsoft/kiota/abstractions/go"
)

// The retry handler handles transient HTTP responses and retries the request given the retry options
type RetryHandler struct {
// default options to use when evaluating the response
options RetryHandlerOptions
}

// Creates a new RetryHandler with default options
func NewRetryHandler() *RetryHandler {
return NewRetryHandlerWithOptions(RetryHandlerOptions{
ShouldRetry: func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool {
Expand All @@ -21,18 +24,25 @@ func NewRetryHandler() *RetryHandler {
})
}

// Creates a new RetryHandler with the given options
// Parameters:
// options: the options to use for the RetryHandler during execution
func NewRetryHandlerWithOptions(options RetryHandlerOptions) *RetryHandler {
return &RetryHandler{options: options}
}

var DEFAULT_MAX_RETRIES = 3
var ABSOLUTE_MAX_RETRIES = 10
var DEFAULT_DELAY_SECONDS = 3
var ABSOLUTE_MAX_DELAY_SECONDS = 180
const defaultMaxRetries = 3
const absoluteMaxRetries = 10
const defaultDelaySeconds = 3
const absoluteMaxDelaySeconds = 180

// Options to apply when evaluating the response for retrial
type RetryHandlerOptions struct {
ShouldRetry func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool
MaxRetries int
// Callback to determine if the request should be retried
ShouldRetry func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool
// The maximum number of times a request can be retried
MaxRetries int
// The delay in seconds between retries
DelaySeconds int
}

Expand All @@ -47,37 +57,44 @@ var retryKeyValue = abs.RequestOptionKey{
Key: "RetryHandler",
}

// Returns the key value to be used when the option is added to the request context
func (o *RetryHandlerOptions) GetKey() abs.RequestOptionKey {
return retryKeyValue
}

// Returns the should retry callback function
func (o *RetryHandlerOptions) GetShouldRetry() func(delay time.Duration, executionCount int, request *nethttp.Request, response *nethttp.Response) bool {
return o.ShouldRetry
}

// Returns the delays in seconds between retries
func (o *RetryHandlerOptions) GetDelaySeconds() int {
if o.DelaySeconds < 1 {
return DEFAULT_DELAY_SECONDS
} else if o.DelaySeconds > ABSOLUTE_MAX_DELAY_SECONDS {
return ABSOLUTE_MAX_DELAY_SECONDS
return defaultDelaySeconds
} else if o.DelaySeconds > absoluteMaxDelaySeconds {
return absoluteMaxDelaySeconds
} else {
return o.DelaySeconds
}
}

// Returns the maximum number of times a request can be retried
func (o *RetryHandlerOptions) GetMaxRetries() int {
if o.MaxRetries < 1 {
return DEFAULT_MAX_RETRIES
} else if o.MaxRetries > ABSOLUTE_MAX_RETRIES {
return ABSOLUTE_MAX_RETRIES
return defaultMaxRetries
} else if o.MaxRetries > absoluteMaxRetries {
return absoluteMaxRetries
} else {
return o.MaxRetries
}
}

const RETRY_ATTEMPT_HEADER = "Retry-Attempt"
const RETRY_AFTER_HEADER = "Retry-After"
const retryAttemptHeader = "Retry-Attempt"
const retryAfterHeader = "Retry-After"

var TOO_MANY_REQUESTS = 429
var SERVICE_UNAVAILABLE = 503
var GATEWAY_TIMEOUT = 504
const tooManyRequests = 429
const serviceUnavailable = 503
const gatewayTimeout = 504

func (middleware RetryHandler) Intercept(pipeline Pipeline, req *nethttp.Request) (*nethttp.Response, error) {
response, err := pipeline.Next(req)
Expand All @@ -95,12 +112,12 @@ func (middleware RetryHandler) retryRequest(pipeline Pipeline, options retryHand
if middleware.isRetriableErrorCode(resp.StatusCode) &&
middleware.isRetriableRequest(req) &&
executionCount < options.GetMaxRetries() &&
cummulativeDelay < time.Duration(ABSOLUTE_MAX_DELAY_SECONDS)*time.Second &&
cummulativeDelay < time.Duration(absoluteMaxDelaySeconds)*time.Second &&
options.GetShouldRetry()(cummulativeDelay, executionCount, req, resp) {
executionCount++
delay := middleware.getRetryDelay(req, resp, options, executionCount)
cummulativeDelay += delay
req.Header.Set(RETRY_ATTEMPT_HEADER, strconv.Itoa(executionCount))
req.Header.Set(retryAttemptHeader, strconv.Itoa(executionCount))
time.Sleep(delay)
response, err := pipeline.Next(req)
if err != nil {
Expand All @@ -112,7 +129,7 @@ func (middleware RetryHandler) retryRequest(pipeline Pipeline, options retryHand
}

func (middleware RetryHandler) isRetriableErrorCode(code int) bool {
return code == TOO_MANY_REQUESTS || code == SERVICE_UNAVAILABLE || code == GATEWAY_TIMEOUT
return code == tooManyRequests || code == serviceUnavailable || code == gatewayTimeout
}
func (middleware RetryHandler) isRetriableRequest(req *nethttp.Request) bool {
isBodiedMethod := req.Method == "POST" || req.Method == "PUT" || req.Method == "PATCH"
Expand All @@ -123,7 +140,7 @@ func (middleware RetryHandler) isRetriableRequest(req *nethttp.Request) bool {
}

func (middleware RetryHandler) getRetryDelay(req *nethttp.Request, resp *nethttp.Response, options retryHandlerOptionsInt, executionCount int) time.Duration {
retryAfter := resp.Header.Get(RETRY_AFTER_HEADER)
retryAfter := resp.Header.Get(retryAfterHeader)
if retryAfter != "" {
retryAfterDelay, err := strconv.ParseFloat(retryAfter, 64)
if err == nil {
Expand Down
2 changes: 1 addition & 1 deletion http/go/nethttp/retry_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestItHonoursMaxRetries(t *testing.T) {
}
assert.NotNil(t, resp)
assert.Equal(t, 429, resp.StatusCode)
assert.Equal(t, DEFAULT_MAX_RETRIES, retryAttemptInt)
assert.Equal(t, defaultMaxRetries, retryAttemptInt)
}

func TestItDoesntRetryOnSuccess(t *testing.T) {
Expand Down

0 comments on commit 5032c16

Please sign in to comment.