Skip to content

Commit eeb231c

Browse files
committed
reverse_proxy: deprecate RetryMatchRaw in favor of RetryConditionsRaw
1 parent 4e31ada commit eeb231c

File tree

3 files changed

+116
-34
lines changed

3 files changed

+116
-34
lines changed

modules/caddyhttp/reverseproxy/caddyfile.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -333,14 +333,13 @@ func (h *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
333333
if err != nil {
334334
return err
335335
}
336-
if condSet != nil {
337-
if matcherSet != nil {
338-
condSet.MatchRaw = matcherSet
339-
}
340-
h.LoadBalancing.RetryConditionsRaw = append(h.LoadBalancing.RetryConditionsRaw, condSet)
341-
} else if matcherSet != nil {
342-
h.LoadBalancing.RetryMatchRaw = append(h.LoadBalancing.RetryMatchRaw, matcherSet)
336+
if condSet == nil {
337+
condSet = new(RetryConditionSet)
338+
}
339+
if matcherSet != nil {
340+
condSet.MatchRaw = matcherSet
343341
}
342+
h.LoadBalancing.RetryConditionsRaw = append(h.LoadBalancing.RetryConditionsRaw, condSet)
344343

345344
case "health_uri":
346345
if !d.NextArg() {

modules/caddyhttp/reverseproxy/retries_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,3 +499,72 @@ func TestLbRetryMatchCaddyfileParsing(t *testing.T) {
499499
})
500500
}
501501
}
502+
503+
func TestLbRetryMatchBackwardCompatibility(t *testing.T) {
504+
// Plain lb_retry_match blocks (without status) should go into
505+
// RetryConditionsRaw with matchers but no status codes, preserving
506+
// the pre-existing behavior.
507+
t.Run("plain matcher block goes into RetryConditionsRaw", func(t *testing.T) {
508+
d := caddyfile.NewTestDispenser(`reverse_proxy localhost:8080 {
509+
lb_retry_match {
510+
method POST
511+
}
512+
}`)
513+
h := new(Handler)
514+
if err := h.UnmarshalCaddyfile(d); err != nil {
515+
t.Fatalf("unexpected error: %v", err)
516+
}
517+
if h.LoadBalancing == nil {
518+
t.Fatal("LoadBalancing is nil")
519+
}
520+
if len(h.LoadBalancing.RetryMatchRaw) != 0 {
521+
t.Errorf("RetryMatchRaw should be empty, got %d entries", len(h.LoadBalancing.RetryMatchRaw))
522+
}
523+
if len(h.LoadBalancing.RetryConditionsRaw) != 1 {
524+
t.Fatalf("RetryConditionsRaw length: got %d, want 1", len(h.LoadBalancing.RetryConditionsRaw))
525+
}
526+
rc := h.LoadBalancing.RetryConditionsRaw[0]
527+
if len(rc.Status) != 0 {
528+
t.Errorf("Status should be empty, got %v", rc.Status)
529+
}
530+
if len(rc.MatchRaw) == 0 {
531+
t.Fatal("MatchRaw should contain the method matcher")
532+
}
533+
if _, ok := rc.MatchRaw["method"]; !ok {
534+
t.Errorf("MatchRaw should have 'method' key, got keys: %v", rc.MatchRaw)
535+
}
536+
})
537+
538+
// Multiple blocks: plain + status, all go into RetryConditionsRaw
539+
t.Run("mixed blocks all go into RetryConditionsRaw", func(t *testing.T) {
540+
d := caddyfile.NewTestDispenser(`reverse_proxy localhost:8080 {
541+
lb_retry_match {
542+
method PUT
543+
}
544+
lb_retry_match {
545+
status 503
546+
}
547+
}`)
548+
h := new(Handler)
549+
if err := h.UnmarshalCaddyfile(d); err != nil {
550+
t.Fatalf("unexpected error: %v", err)
551+
}
552+
if len(h.LoadBalancing.RetryMatchRaw) != 0 {
553+
t.Errorf("RetryMatchRaw should be empty, got %d entries", len(h.LoadBalancing.RetryMatchRaw))
554+
}
555+
if len(h.LoadBalancing.RetryConditionsRaw) != 2 {
556+
t.Fatalf("RetryConditionsRaw length: got %d, want 2", len(h.LoadBalancing.RetryConditionsRaw))
557+
}
558+
// Block 1: plain matcher, no status
559+
if len(h.LoadBalancing.RetryConditionsRaw[0].Status) != 0 {
560+
t.Errorf("block 0 should have no status codes")
561+
}
562+
if _, ok := h.LoadBalancing.RetryConditionsRaw[0].MatchRaw["method"]; !ok {
563+
t.Errorf("block 0 should have method matcher")
564+
}
565+
// Block 2: status, no matchers
566+
if len(h.LoadBalancing.RetryConditionsRaw[1].Status) != 1 || h.LoadBalancing.RetryConditionsRaw[1].Status[0] != 503 {
567+
t.Errorf("block 1 should have status [503], got %v", h.LoadBalancing.RetryConditionsRaw[1].Status)
568+
}
569+
})
570+
}

modules/caddyhttp/reverseproxy/reverseproxy.go

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -382,14 +382,14 @@ func (h *Handler) Provision(ctx caddy.Context) error {
382382
// defaulting to a sane wait period between attempts
383383
h.LoadBalancing.TryInterval = caddy.Duration(250 * time.Millisecond)
384384
}
385-
lbMatcherSets, err := ctx.LoadModule(h.LoadBalancing, "RetryMatchRaw")
386-
if err != nil {
387-
return err
388-
}
389-
err = h.LoadBalancing.RetryMatch.FromInterface(lbMatcherSets)
390-
if err != nil {
391-
return err
385+
// convert legacy retry_match entries into retry_conditions for
386+
// backward compatibility with the JSON API
387+
for _, matcherSetRaw := range h.LoadBalancing.RetryMatchRaw {
388+
h.LoadBalancing.RetryConditionsRaw = append(h.LoadBalancing.RetryConditionsRaw, &RetryConditionSet{
389+
MatchRaw: matcherSetRaw,
390+
})
392391
}
392+
h.LoadBalancing.RetryMatchRaw = nil
393393

394394
// provision retry conditions
395395
for i, rc := range h.LoadBalancing.RetryConditionsRaw {
@@ -1325,16 +1325,10 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
13251325
if !isDialError && (!isHandlerError || !errors.Is(herr, errNoUpstream)) {
13261326
// retryableStatusError is always safe to retry (we control when it's generated)
13271327
if _, ok := proxyErr.(retryableStatusError); !ok {
1328-
// default behavior: retry GET or RetryMatch
1329-
if lb.RetryMatch == nil && req.Method != "GET" {
1328+
if len(lb.retryConditions) == 0 && req.Method != "GET" {
13301329
return false
13311330
}
1332-
match, err := lb.RetryMatch.AnyMatchWithError(req)
1333-
if err != nil {
1334-
logger.Error("error matching request for retry", zap.Error(err))
1335-
return false
1336-
}
1337-
if !match {
1331+
if len(lb.retryConditions) > 0 && !lb.matchesRetryCondition(req, logger) {
13381332
return false
13391333
}
13401334
}
@@ -1360,6 +1354,26 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
13601354
}
13611355
}
13621356

1357+
// matchesRetryCondition checks whether any configured retry condition's
1358+
// request matchers match the given request. If a condition has no matchers,
1359+
// any request matches it.
1360+
func (lb LoadBalancing) matchesRetryCondition(req *http.Request, logger *zap.Logger) bool {
1361+
for _, rc := range lb.retryConditions {
1362+
if len(rc.matchers) == 0 {
1363+
return true
1364+
}
1365+
match, err := rc.matchers.MatchWithError(req)
1366+
if err != nil {
1367+
logger.Error("error matching request for retry condition", zap.Error(err))
1368+
continue
1369+
}
1370+
if match {
1371+
return true
1372+
}
1373+
}
1374+
return false
1375+
}
1376+
13631377
// directRequest modifies only req.URL so that it points to the upstream
13641378
// in the given DialInfo. It must modify ONLY the request URL.
13651379
func (h *Handler) directRequest(req *http.Request, di DialInfo) {
@@ -1610,23 +1624,23 @@ type LoadBalancing struct {
16101624
// to spin if all backends are down and latency is very low.
16111625
TryInterval caddy.Duration `json:"try_interval,omitempty"`
16121626

1613-
// A list of matcher sets that restricts with which requests retries are
1614-
// allowed. A request must match any of the given matcher sets in order
1615-
// to be retried if the connection to the upstream succeeded but the
1616-
// subsequent round-trip failed. If the connection to the upstream failed,
1617-
// a retry is always allowed. If unspecified, only GET requests will be
1618-
// allowed to be retried. Note that a retry is done with the next available
1619-
// host according to the load balancing policy.
1627+
// Deprecated: Use RetryConditionsRaw instead. Kept for backward
1628+
// compatibility with the JSON API. Entries are converted to
1629+
// RetryConditionsRaw during provisioning.
16201630
RetryMatchRaw caddyhttp.RawMatcherSets `json:"retry_match,omitempty" caddy:"namespace=http.matchers"`
16211631

16221632
// A list of retry condition sets. Each set bundles request matchers
1623-
// with optional status codes. A retry is triggered if the upstream
1624-
// responds with a matching status code. Within a set, the request
1625-
// must also match the matchers (if any).
1633+
// with optional status codes. A request must match any of the given
1634+
// condition sets in order to be retried if the connection to the
1635+
// upstream succeeded but the subsequent round-trip failed. If the
1636+
// connection to the upstream failed, a retry is always allowed. If
1637+
// a condition has status codes, the upstream's response status must
1638+
// match one of them for a retry to be triggered. If unspecified,
1639+
// only GET requests will be retried. Note that a retry is done with
1640+
// the next available host according to the load balancing policy.
16261641
RetryConditionsRaw []*RetryConditionSet `json:"retry_conditions,omitempty"`
16271642

1628-
SelectionPolicy Selector `json:"-"`
1629-
RetryMatch caddyhttp.MatcherSets `json:"-"`
1643+
SelectionPolicy Selector `json:"-"`
16301644
retryConditions []*retryCondition
16311645
}
16321646

0 commit comments

Comments
 (0)