Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 03:30 UTC
caddyhttp: skip matchers for satisfied route groups (#8189)
* caddyhttp: Skip matchers of grouped routes once the group is satisfied * caddyhttp: Test that a skipped grouped route keeps regexp captures A later rewrite in the same group must not run its path_regexp matcher and overwrite {re.x.1} from the rewrite that was used.
Jay Hemnani committed
Oct 11, 2026 at 02:20 UTC
5538b947d2873e8a06dd45182b8c43081fc56d32
2 files changed
+84
-12
caddytest/integration/try_files_test.go
new
+69
@@ -0,0 +1,69 @@
1
+package integration
2
+
3
+import (
4
+ "fmt"
5
+ "net/http"
6
+ "os"
7
+ "path/filepath"
8
+ "testing"
9
+
10
+ "github.com/caddyserver/caddy/v2/caddytest"
11
+)
12
+
13
+func TestTryFilesErrorCodeAfterQueryItem(t *testing.T) {
14
+ root := t.TempDir()
15
+ for name, content := range map[string]string{
16
+ "style.css": "css",
17
+ "about.html": "about",
18
+ } {
19
+ if err := os.WriteFile(filepath.Join(root, name), []byte(content), 0o600); err != nil {
20
+ t.Fatal(err)
21
+ }
22
+ }
23
+
24
+ tester := caddytest.NewTester(t)
25
+ tester.InitServer(fmt.Sprintf(`
26
+ {
27
+ skip_install_trust
28
+ admin localhost:2999
29
+ http_port 9080
30
+ grace_period 1ns
31
+ }
32
+ http://localhost:9080 {
33
+ root * %q
34
+ try_files {path} {path}.html?{query} =404
35
+ file_server
36
+ }
37
+ `, filepath.ToSlash(root)), "caddyfile")
38
+
39
+ tester.AssertGetResponse("http://localhost:9080/style.css", 200, "css")
40
+ tester.AssertGetResponse("http://localhost:9080/about?x=1", 200, "about")
41
+
42
+ req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/missing", nil)
43
+ if err != nil {
44
+ t.Fatal(err)
45
+ }
46
+ tester.AssertResponseCode(req, http.StatusNotFound)
47
+}
48
+
49
+func TestSkippedGroupedRouteKeepsRegexpCapture(t *testing.T) {
50
+ tester := caddytest.NewTester(t)
51
+ tester.InitServer(`
52
+ {
53
+ skip_install_trust
54
+ admin localhost:2999
55
+ http_port 9080
56
+ grace_period 1ns
57
+ }
58
+ http://localhost:9080 {
59
+ @x path_regexp x ^/x/(.*)$
60
+ @y path_regexp x ^/(.*)$
61
+ rewrite @x /one/{re.x.1}
62
+ rewrite @y /two/{re.x.1}
63
+ respond "path={path} cap={re.x.1}"
64
+ }
65
+ `, "caddyfile")
66
+
67
+ tester.AssertGetResponse("http://localhost:9080/x/q", 200, "path=/one/q cap=q")
68
+ tester.AssertGetResponse("http://localhost:9080/z", 200, "path=/two/z cap=z")
69
+}
modules/caddyhttp/routes.go
+15
-12
@@ -268,6 +268,19 @@ func wrapRoute(route Route) Middleware {
268
// but I just thought this made more sense
269
nextCopy := next
270
271
+ // if route is part of a group, ensure only the
272
+ // first matching route in the group is applied
273
+ var groups map[string]struct{}
274
+ if route.Group != "" {
275
+ groups = req.Context().Value(routeGroupCtxKey).(map[string]struct{})
276
+
277
+ if _, ok := groups[route.Group]; ok {
278
+ // this group has already been satisfied by a
279
+ // matching route; skip without running matchers
280
+ return nextCopy.ServeHTTP(rw, req)
281
+ }
282
+ }
283
+
284
// route must match at least one of the matcher sets
285
matches, err := route.MatcherSets.AnyMatchWithError(req)
286
if err != nil {
@@ -281,18 +294,8 @@ func wrapRoute(route Route) Middleware {
294
return nextCopy.ServeHTTP(rw, req)
295
}
296
284
- // if route is part of a group, ensure only the
285
- // first matching route in the group is applied
286
- if route.Group != "" {
287
- groups := req.Context().Value(routeGroupCtxKey).(map[string]struct{})
288
-
289
- if _, ok := groups[route.Group]; ok {
290
- // this group has already been
291
- // satisfied by a matching route
292
- return nextCopy.ServeHTTP(rw, req)
293
- }
294
-
295
- // this matching route satisfies the group
297
+ // this matching route satisfies the group
298
+ if groups != nil {
299
groups[route.Group] = struct{}{}
300
}
301