@marcel91200 / caddy Mirror · read-only / commits / be7abda7
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 14, 05:15 UTC

reverse_proxy: Implement retry_match; by default only retry GET requests

See https://caddy.community/t/http-proxy-and-non-get-retries/6304

Matthew Holt committed Oct 5, 2019 at 16:22 UTC be7abda7d498272e360fe6fae18984c309533de3
2 files changed +58 -20
modules/caddyhttp/reverseproxy/httptransport.go
+8 -1
@@ -79,7 +79,14 @@ func (h *HTTPTransport) Provision(_ caddy.Context) error {
79 network = dialInfo.Network
80 address = dialInfo.Address
81 }
82 - return dialer.DialContext(ctx, network, address)
82 + conn, err := dialer.DialContext(ctx, network, address)
83 + if err != nil {
84 + // identify this error as one that occurred during
85 + // dialing, which can be important when trying to
86 + // decide whether to retry a request
87 + return nil, DialError{err}
88 + }
89 + return conn, nil
90 },
91 MaxConnsPerHost: h.MaxConnsPerHost,
92 ResponseHeaderTimeout: time.Duration(h.ResponseHeaderTimeout),
modules/caddyhttp/reverseproxy/reverseproxy.go
+50 -19
@@ -115,6 +115,12 @@ func (h *Handler) Provision(ctx caddy.Context) error {
115 // defaulting to a sane wait period between attempts
116 h.LoadBalancing.TryInterval = caddy.Duration(250 * time.Millisecond)
117 }
118 + lbMatcherSets, err := h.LoadBalancing.RetryMatchRaw.Setup(ctx)
119 + if err != nil {
120 + return err
121 + }
122 + h.LoadBalancing.RetryMatch = lbMatcherSets
123 + h.LoadBalancing.RetryMatchRaw = nil // allow GC to deallocate
124
125 // if active health checks are enabled, configure them and start a worker
126 if h.HealthChecks != nil &&
@@ -270,7 +276,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
276 if proxyErr == nil {
277 proxyErr = fmt.Errorf("no upstreams available")
278 }
273 - if !h.tryAgain(start, proxyErr) {
279 + if !h.LoadBalancing.tryAgain(start, proxyErr, r) {
280 break
281 }
282 continue
@@ -306,7 +312,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
312 proxyErr = h.reverseProxy(w, r, upstream)
313 if proxyErr == nil || proxyErr == context.Canceled {
314 // context.Canceled happens when the downstream client
309 - // cancels the request; we don't have to worry about that
315 + // cancels the request, which is not our failure
316 return nil
317 }
318
@@ -314,7 +320,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
320 h.countFailure(upstream)
321
322 // if we've tried long enough, break
317 - if !h.tryAgain(start, proxyErr) {
323 + if !h.LoadBalancing.tryAgain(start, proxyErr, r) {
324 break
325 }
326 }
@@ -510,22 +516,39 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, upstre
516 }
517
518 // tryAgain takes the time that the handler was initially invoked
513 -// as well as any error currently obtained and returns true if
514 -// another attempt should be made at proxying the request. If
515 -// true is returned, it has already blocked long enough before
516 -// the next retry (i.e. no more sleeping is needed). If false is
517 -// returned, the handler should stop trying to proxy the request.
518 -func (h Handler) tryAgain(start time.Time, proxyErr error) bool {
519 - // if downstream has canceled the request, break
520 - if proxyErr == context.Canceled {
521 - return false
522 - }
519 +// as well as any error currently obtained, and the request being
520 +// tried, and returns true if another attempt should be made at
521 +// proxying the request. If true is returned, it has already blocked
522 +// long enough before the next retry (i.e. no more sleeping is
523 +// needed). If false is returned, the handler should stop trying to
524 +// proxy the request.
525 +func (lb LoadBalancing) tryAgain(start time.Time, proxyErr error, req *http.Request) bool {
526 // if we've tried long enough, break
524 - if time.Since(start) >= time.Duration(h.LoadBalancing.TryDuration) {
527 + if time.Since(start) >= time.Duration(lb.TryDuration) {
528 return false
529 }
530 +
531 + // if the error occurred while dialing (i.e. a connection
532 + // could not even be established to the upstream), then it
533 + // should be safe to retry, since without a connection, no
534 + // HTTP request can be transmitted; but if the error is not
535 + // specifically a dialer error, we need to be careful
536 + if _, ok := proxyErr.(DialError); proxyErr != nil && !ok {
537 + // if the error occurred after a connection was established,
538 + // we have to assume the upstream received the request, and
539 + // retries need to be carefully decided, because some requests
540 + // are not idempotent
541 + if lb.RetryMatch == nil && req.Method != "GET" {
542 + // by default, don't retry requests if they aren't GET
543 + return false
544 + }
545 + if !lb.RetryMatch.AnyMatch(req) {
546 + return false
547 + }
548 + }
549 +
550 // otherwise, wait and try the next available host
528 - time.Sleep(time.Duration(h.LoadBalancing.TryInterval))
551 + time.Sleep(time.Duration(lb.TryInterval))
552 return true
553 }
554
@@ -621,11 +644,13 @@ func removeConnectionHeaders(h http.Header) {
644
645 // LoadBalancing has parameters related to load balancing.
646 type LoadBalancing struct {
624 - SelectionPolicyRaw json.RawMessage `json:"selection_policy,omitempty"`
625 - TryDuration caddy.Duration `json:"try_duration,omitempty"`
626 - TryInterval caddy.Duration `json:"try_interval,omitempty"`
647 + SelectionPolicyRaw json.RawMessage `json:"selection_policy,omitempty"`
648 + TryDuration caddy.Duration `json:"try_duration,omitempty"`
649 + TryInterval caddy.Duration `json:"try_interval,omitempty"`
650 + RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty"`
651
628 - SelectionPolicy Selector `json:"-"`
652 + SelectionPolicy Selector `json:"-"`
653 + RetryMatch caddyhttp.MatcherSets `json:"-"`
654 }
655
656 // Selector selects an available upstream from the pool.
@@ -650,6 +675,12 @@ var hopHeaders = []string{
675 "Upgrade",
676 }
677
678 +// DialError is an error that specifically occurs
679 +// in a call to Dial or DialContext.
680 +type DialError struct {
681 + error
682 +}
683 +
684 // TODO: see if we can use this
685 // var bufPool = sync.Pool{
686 // New: func() interface{} {