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{} {