@marcel91200 / caddy Mirror · read-only / commits / 5538b947
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 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