283
// code matching a retry_match expression, the request is retried on the next
284
// upstream
285
func TestResponseRetryStatusCode(t *testing.T) {
286
- // Bad upstream: returns 502
287
- badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
288
- w.WriteHeader(http.StatusBadGateway)
289
- }))
290
- t.Cleanup(badServer.Close)
286
+ for _, expr := range []string{
287
+ "{http.reverse_proxy.status_code} in [502, 503]",
288
+ `ph(req, "http.reverse_proxy.status_code") in [502, 503]`,
289
+ } {
290
+ t.Run(expr, func(t *testing.T) {
291
+ // Bad upstream: returns 502
292
+ badServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
293
+ w.WriteHeader(http.StatusBadGateway)
294
+ }))
295
+ t.Cleanup(badServer.Close)
296
292
- // Good upstream: returns 200
293
- goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
294
- w.WriteHeader(http.StatusOK)
295
- w.Write([]byte("ok"))
296
- }))
297
- t.Cleanup(goodServer.Close)
297
+ // Good upstream: returns 200
298
+ goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
299
+ w.WriteHeader(http.StatusOK)
300
+ w.Write([]byte("ok"))
301
+ }))
302
+ t.Cleanup(goodServer.Close)
303
299
- retryMatch := caddyhttp.MatcherSets{
300
- caddyhttp.MatcherSet{
301
- newExpressionMatcher(t, "{http.reverse_proxy.status_code} in [502, 503]"),
302
- },
303
- }
304
+ retryMatch := caddyhttp.MatcherSets{
305
+ caddyhttp.MatcherSet{
306
+ newExpressionMatcher(t, expr),
307
+ },
308
+ }
309
305
- // RoundRobin picks index 1 first, then 0
306
- upstreams := []*Upstream{
307
- {Host: new(Host), Dial: goodServer.Listener.Addr().String()},
308
- {Host: new(Host), Dial: badServer.Listener.Addr().String()},
309
- }
310
+ // RoundRobin picks index 1 first, then 0
311
+ upstreams := []*Upstream{
312
+ {Host: new(Host), Dial: goodServer.Listener.Addr().String()},
313
+ {Host: new(Host), Dial: badServer.Listener.Addr().String()},
314
+ }
315
311
- h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
316
+ h := minimalHandlerWithRetryMatch(1, retryMatch, upstreams...)
317
313
- req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
314
- req = prepareTestRequest(req)
315
- rec := httptest.NewRecorder()
318
+ req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
319
+ req = prepareTestRequest(req)
320
+ rec := httptest.NewRecorder()
321
317
- err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
318
- return nil
319
- }))
322
+ err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
323
+ return nil
324
+ }))
325
321
- gotStatus := rec.Code
322
- if err != nil {
323
- if herr, ok := err.(caddyhttp.HandlerError); ok {
324
- gotStatus = herr.StatusCode
325
- }
326
- }
326
+ gotStatus := rec.Code
327
+ if err != nil {
328
+ if herr, ok := err.(caddyhttp.HandlerError); ok {
329
+ gotStatus = herr.StatusCode
330
+ }
331
+ }
332
328
- if gotStatus != http.StatusOK {
329
- t.Errorf("status: got %d, want %d (err=%v)", gotStatus, http.StatusOK, err)
333
+ if gotStatus != http.StatusOK {
334
+ t.Errorf("status: got %d, want %d (err=%v)", gotStatus, http.StatusOK, err)
335
+ }
336
+ })
337
}
338
}
339
572
}
573
}
574
575
+// TestRequestOnlyExpressionDoesNotRetryResponses verifies that an expression
576
+// matcher that only tests the request or the selected upstream (no response
577
+// placeholders) does not cause successful responses to be retried
578
+func TestRequestOnlyExpressionDoesNotRetryResponses(t *testing.T) {
579
+ for _, expr := range []string{
580
+ `{http.request.method} == "POST"`,
581
+ `{http.reverse_proxy.upstream.host} == "127.0.0.1"`,
582
+ `{http.request.uri.path} != "\{http.reverse_proxy.status_code}"`,
583
+ `{http.reverse_proxy.status_codes} == null`,
584
+ } {
585
+ t.Run(expr, func(t *testing.T) {
586
+ var hits atomic.Int32
587
+
588
+ // Server returns 200 OK for all requests
589
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
590
+ hits.Add(1)
591
+ w.WriteHeader(http.StatusOK)
592
+ w.Write([]byte("ok"))
593
+ }))
594
+ t.Cleanup(server.Close)
595
+
596
+ retryMatch := caddyhttp.MatcherSets{
597
+ caddyhttp.MatcherSet{
598
+ newExpressionMatcher(t, expr),
599
+ },
600
+ }
601
+
602
+ upstreams := []*Upstream{
603
+ {Host: new(Host), Dial: server.Listener.Addr().String()},
604
+ {Host: new(Host), Dial: server.Listener.Addr().String()},
605
+ }
606
+
607
+ h := minimalHandlerWithRetryMatch(2, retryMatch, upstreams...)
608
+
609
+ req := httptest.NewRequest(http.MethodPost, "http://example.com/", strings.NewReader("order=1"))
610
+ req = prepareTestRequest(req)
611
+ rec := httptest.NewRecorder()
612
+
613
+ err := h.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
614
+ return nil
615
+ }))
616
+ if err != nil {
617
+ t.Fatalf("unexpected error: %v", err)
618
+ }
619
+
620
+ if hits.Load() != 1 {
621
+ t.Errorf("upstream hits: got %d, want 1 (should not retry successful responses)", hits.Load())
622
+ }
623
+ if rec.Code != http.StatusOK {
624
+ t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
625
+ }
626
+ if rec.Body.String() != "ok" {
627
+ t.Errorf("body: got %q, want %q", rec.Body.String(), "ok")
628
+ }
629
+ })
630
+ }
631
+}
632
+
633
// brokenUpstreamAddr returns the address of a TCP listener that accepts
634
// connections but immediately closes them, causing a transport error (not
635
// a dial error). This simulates an upstream that is reachable but broken