@marcel91200 / caddy Mirror · read-only / commits / 4181c79a
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 12, 18:15 UTC

httpcaddyfile: Add optional status code argument to `handle_errors` directive (#5965)

Co-authored-by: Aziz Rmadi <azizrmadi@Azizs-MacBook-Air.local>

Aziz Rmadi committed Jan 16, 2024 at 00:24 UTC 4181c79a8130a59c40c76437e15265452422ccb1
8 files changed +942 -2
caddyconfig/httpcaddyfile/builtins.go
+58 -1
@@ -844,10 +844,67 @@ func parseHandle(h Helper) (caddyhttp.MiddlewareHandler, error) {
844 }
845
846 func parseHandleErrors(h Helper) ([]ConfigValue, error) {
847 - subroute, err := ParseSegmentAsSubroute(h)
847 + h.Next()
848 + args := h.RemainingArgs()
849 + expression := ""
850 + if len(args) > 0 {
851 + expression = ""
852 + codes := []string{}
853 + for _, val := range args {
854 + if len(val) != 3 {
855 + return nil, h.Errf("bad status value '%s'", val)
856 + }
857 + if strings.HasSuffix(val, "xx") {
858 + val = val[:1]
859 + _, err := strconv.Atoi(val)
860 + if err != nil {
861 + return nil, h.Errf("bad status value '%s': %v", val, err)
862 + }
863 + if expression != "" {
864 + expression += " || "
865 + }
866 + expression += fmt.Sprintf("{http.error.status_code} >= %s00 && {http.error.status_code} <= %s99", val, val)
867 + continue
868 + }
869 + _, err := strconv.Atoi(val)
870 + if err != nil {
871 + return nil, h.Errf("bad status value '%s': %v", val, err)
872 + }
873 + codes = append(codes, val)
874 + }
875 + if len(codes) > 0 {
876 + if expression != "" {
877 + expression += " || "
878 + }
879 + expression += "{http.error.status_code} in [" + strings.Join(codes, ", ") + "]"
880 + }
881 + // Reset cursor position to get ready for ParseSegmentAsSubroute
882 + h.Reset()
883 + h.Next()
884 + h.RemainingArgs()
885 + h.Prev()
886 + } else {
887 + // If no arguments present reset the cursor position to get ready for ParseSegmentAsSubroute
888 + h.Prev()
889 + }
890 +
891 + handler, err := ParseSegmentAsSubroute(h)
892 if err != nil {
893 return nil, err
894 }
895 + subroute, ok := handler.(*caddyhttp.Subroute)
896 + if !ok {
897 + return nil, h.Errf("segment was not parsed as a subroute")
898 + }
899 +
900 + if expression != "" {
901 + statusMatcher := caddy.ModuleMap{
902 + "expression": h.JSON(caddyhttp.MatchExpression{Expr: expression}),
903 + }
904 + for i := range subroute.Routes {
905 + subroute.Routes[i].MatcherSetsRaw = []caddy.ModuleMap{statusMatcher}
906 + }
907 + }
908 return []ConfigValue{
909 {
910 Class: "error_route",
caddyconfig/httpcaddyfile/httptype.go
+10 -1
@@ -774,10 +774,19 @@ func (st *ServerType) serversFromPairings(
774 if srv.Errors == nil {
775 srv.Errors = new(caddyhttp.HTTPErrorConfig)
776 }
777 + sort.SliceStable(errorSubrouteVals, func(i, j int) bool {
778 + sri, srj := errorSubrouteVals[i].Value.(*caddyhttp.Subroute), errorSubrouteVals[j].Value.(*caddyhttp.Subroute)
779 + if len(sri.Routes[0].MatcherSetsRaw) == 0 && len(srj.Routes[0].MatcherSetsRaw) != 0 {
780 + return false
781 + }
782 + return true
783 + })
784 + errorsSubroute := &caddyhttp.Subroute{}
785 for _, val := range errorSubrouteVals {
786 sr := val.Value.(*caddyhttp.Subroute)
779 - srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, sr, matcherSetsEnc, p, warnings)
787 + errorsSubroute.Routes = append(errorsSubroute.Routes, sr.Routes...)
788 }
789 + srv.Errors.Routes = appendSubrouteToRouteList(srv.Errors.Routes, errorsSubroute, matcherSetsEnc, p, warnings)
790 }
791
792 // add log associations
caddytest/integration/caddyfile_adapt/error_multi_site_blocks.txt new
+245
@@ -0,0 +1,245 @@
1 +foo.localhost {
2 + root * /srv
3 + error /private* "Unauthorized" 410
4 + error /fivehundred* "Internal Server Error" 500
5 +
6 + handle_errors 5xx {
7 + respond "Error In range [500 .. 599]"
8 + }
9 + handle_errors 410 {
10 + respond "404 or 410 error"
11 + }
12 +}
13 +
14 +bar.localhost {
15 + root * /srv
16 + error /private* "Unauthorized" 410
17 + error /fivehundred* "Internal Server Error" 500
18 +
19 + handle_errors 5xx {
20 + respond "Error In range [500 .. 599] from second site"
21 + }
22 + handle_errors 410 {
23 + respond "404 or 410 error from second site"
24 + }
25 +}
26 +----------
27 +{
28 + "apps": {
29 + "http": {
30 + "servers": {
31 + "srv0": {
32 + "listen": [
33 + ":443"
34 + ],
35 + "routes": [
36 + {
37 + "match": [
38 + {
39 + "host": [
40 + "foo.localhost"
41 + ]
42 + }
43 + ],
44 + "handle": [
45 + {
46 + "handler": "subroute",
47 + "routes": [
48 + {
49 + "handle": [
50 + {
51 + "handler": "vars",
52 + "root": "/srv"
53 + }
54 + ]
55 + },
56 + {
57 + "handle": [
58 + {
59 + "error": "Internal Server Error",
60 + "handler": "error",
61 + "status_code": 500
62 + }
63 + ],
64 + "match": [
65 + {
66 + "path": [
67 + "/fivehundred*"
68 + ]
69 + }
70 + ]
71 + },
72 + {
73 + "handle": [
74 + {
75 + "error": "Unauthorized",
76 + "handler": "error",
77 + "status_code": 410
78 + }
79 + ],
80 + "match": [
81 + {
82 + "path": [
83 + "/private*"
84 + ]
85 + }
86 + ]
87 + }
88 + ]
89 + }
90 + ],
91 + "terminal": true
92 + },
93 + {
94 + "match": [
95 + {
96 + "host": [
97 + "bar.localhost"
98 + ]
99 + }
100 + ],
101 + "handle": [
102 + {
103 + "handler": "subroute",
104 + "routes": [
105 + {
106 + "handle": [
107 + {
108 + "handler": "vars",
109 + "root": "/srv"
110 + }
111 + ]
112 + },
113 + {
114 + "handle": [
115 + {
116 + "error": "Internal Server Error",
117 + "handler": "error",
118 + "status_code": 500
119 + }
120 + ],
121 + "match": [
122 + {
123 + "path": [
124 + "/fivehundred*"
125 + ]
126 + }
127 + ]
128 + },
129 + {
130 + "handle": [
131 + {
132 + "error": "Unauthorized",
133 + "handler": "error",
134 + "status_code": 410
135 + }
136 + ],
137 + "match": [
138 + {
139 + "path": [
140 + "/private*"
141 + ]
142 + }
143 + ]
144 + }
145 + ]
146 + }
147 + ],
148 + "terminal": true
149 + }
150 + ],
151 + "errors": {
152 + "routes": [
153 + {
154 + "match": [
155 + {
156 + "host": [
157 + "foo.localhost"
158 + ]
159 + }
160 + ],
161 + "handle": [
162 + {
163 + "handler": "subroute",
164 + "routes": [
165 + {
166 + "handle": [
167 + {
168 + "body": "404 or 410 error",
169 + "handler": "static_response"
170 + }
171 + ],
172 + "match": [
173 + {
174 + "expression": "{http.error.status_code} in [410]"
175 + }
176 + ]
177 + },
178 + {
179 + "handle": [
180 + {
181 + "body": "Error In range [500 .. 599]",
182 + "handler": "static_response"
183 + }
184 + ],
185 + "match": [
186 + {
187 + "expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599"
188 + }
189 + ]
190 + }
191 + ]
192 + }
193 + ],
194 + "terminal": true
195 + },
196 + {
197 + "match": [
198 + {
199 + "host": [
200 + "bar.localhost"
201 + ]
202 + }
203 + ],
204 + "handle": [
205 + {
206 + "handler": "subroute",
207 + "routes": [
208 + {
209 + "handle": [
210 + {
211 + "body": "404 or 410 error from second site",
212 + "handler": "static_response"
213 + }
214 + ],
215 + "match": [
216 + {
217 + "expression": "{http.error.status_code} in [410]"
218 + }
219 + ]
220 + },
221 + {
222 + "handle": [
223 + {
224 + "body": "Error In range [500 .. 599] from second site",
225 + "handler": "static_response"
226 + }
227 + ],
228 + "match": [
229 + {
230 + "expression": "{http.error.status_code} \u003e= 500 \u0026\u0026 {http.error.status_code} \u003c= 599"
231 + }
232 + ]
233 + }
234 + ]
235 + }
236 + ],
237 + "terminal": true
238 + }
239 + ]
240 + }
241 + }
242 + }
243 + }
244 + }
245 +}
caddytest/integration/caddyfile_adapt/error_range_codes.txt new
+120
@@ -0,0 +1,120 @@
1 +{
2 + http_port 3010
3 +}
4 +localhost:3010 {
5 + root * /srv
6 + error /private* "Unauthorized" 410
7 + error /hidden* "Not found" 404
8 +
9 + handle_errors 4xx {
10 + respond "Error in the [400 .. 499] range"
11 + }
12 +}
13 +----------
14 +{
15 + "apps": {
16 + "http": {
17 + "http_port": 3010,
18 + "servers": {
19 + "srv0": {
20 + "listen": [
21 + ":3010"
22 + ],
23 + "routes": [
24 + {
25 + "match": [
26 + {
27 + "host": [
28 + "localhost"
29 + ]
30 + }
31 + ],
32 + "handle": [
33 + {
34 + "handler": "subroute",
35 + "routes": [
36 + {
37 + "handle": [
38 + {
39 + "handler": "vars",
40 + "root": "/srv"
41 + }
42 + ]
43 + },
44 + {
45 + "handle": [
46 + {
47 + "error": "Unauthorized",
48 + "handler": "error",
49 + "status_code": 410
50 + }
51 + ],
52 + "match": [
53 + {
54 + "path": [
55 + "/private*"
56 + ]
57 + }
58 + ]
59 + },
60 + {
61 + "handle": [
62 + {
63 + "error": "Not found",
64 + "handler": "error",
65 + "status_code": 404
66 + }
67 + ],
68 + "match": [
69 + {
70 + "path": [
71 + "/hidden*"
72 + ]
73 + }
74 + ]
75 + }
76 + ]
77 + }
78 + ],
79 + "terminal": true
80 + }
81 + ],
82 + "errors": {
83 + "routes": [
84 + {
85 + "match": [
86 + {
87 + "host": [
88 + "localhost"
89 + ]
90 + }
91 + ],
92 + "handle": [
93 + {
94 + "handler": "subroute",
95 + "routes": [
96 + {
97 + "handle": [
98 + {
99 + "body": "Error in the [400 .. 499] range",
100 + "handler": "static_response"
101 + }
102 + ],
103 + "match": [
104 + {
105 + "expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
106 + }
107 + ]
108 + }
109 + ]
110 + }
111 + ],
112 + "terminal": true
113 + }
114 + ]
115 + }
116 + }
117 + }
118 + }
119 + }
120 +}
caddytest/integration/caddyfile_adapt/error_range_simple_codes.txt new
+153
@@ -0,0 +1,153 @@
1 +{
2 + http_port 2099
3 +}
4 +localhost:2099 {
5 + root * /srv
6 + error /private* "Unauthorized" 410
7 + error /threehundred* "Moved Permanently" 301
8 + error /internalerr* "Internal Server Error" 500
9 +
10 + handle_errors 500 3xx {
11 + respond "Error code is equal to 500 or in the [300..399] range"
12 + }
13 + handle_errors 4xx {
14 + respond "Error in the [400 .. 499] range"
15 + }
16 +}
17 +----------
18 +{
19 + "apps": {
20 + "http": {
21 + "http_port": 2099,
22 + "servers": {
23 + "srv0": {
24 + "listen": [
25 + ":2099"
26 + ],
27 + "routes": [
28 + {
29 + "match": [
30 + {
31 + "host": [
32 + "localhost"
33 + ]
34 + }
35 + ],
36 + "handle": [
37 + {
38 + "handler": "subroute",
39 + "routes": [
40 + {
41 + "handle": [
42 + {
43 + "handler": "vars",
44 + "root": "/srv"
45 + }
46 + ]
47 + },
48 + {
49 + "handle": [
50 + {
51 + "error": "Moved Permanently",
52 + "handler": "error",
53 + "status_code": 301
54 + }
55 + ],
56 + "match": [
57 + {
58 + "path": [
59 + "/threehundred*"
60 + ]
61 + }
62 + ]
63 + },
64 + {
65 + "handle": [
66 + {
67 + "error": "Internal Server Error",
68 + "handler": "error",
69 + "status_code": 500
70 + }
71 + ],
72 + "match": [
73 + {
74 + "path": [
75 + "/internalerr*"
76 + ]
77 + }
78 + ]
79 + },
80 + {
81 + "handle": [
82 + {
83 + "error": "Unauthorized",
84 + "handler": "error",
85 + "status_code": 410
86 + }
87 + ],
88 + "match": [
89 + {
90 + "path": [
91 + "/private*"
92 + ]
93 + }
94 + ]
95 + }
96 + ]
97 + }
98 + ],
99 + "terminal": true
100 + }
101 + ],
102 + "errors": {
103 + "routes": [
104 + {
105 + "match": [
106 + {
107 + "host": [
108 + "localhost"
109 + ]
110 + }
111 + ],
112 + "handle": [
113 + {
114 + "handler": "subroute",
115 + "routes": [
116 + {
117 + "handle": [
118 + {
119 + "body": "Error in the [400 .. 499] range",
120 + "handler": "static_response"
121 + }
122 + ],
123 + "match": [
124 + {
125 + "expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
126 + }
127 + ]
128 + },
129 + {
130 + "handle": [
131 + {
132 + "body": "Error code is equal to 500 or in the [300..399] range",
133 + "handler": "static_response"
134 + }
135 + ],
136 + "match": [
137 + {
138 + "expression": "{http.error.status_code} \u003e= 300 \u0026\u0026 {http.error.status_code} \u003c= 399 || {http.error.status_code} in [500]"
139 + }
140 + ]
141 + }
142 + ]
143 + }
144 + ],
145 + "terminal": true
146 + }
147 + ]
148 + }
149 + }
150 + }
151 + }
152 + }
153 +}
caddytest/integration/caddyfile_adapt/error_simple_codes.txt new
+120
@@ -0,0 +1,120 @@
1 +{
2 + http_port 3010
3 +}
4 +localhost:3010 {
5 + root * /srv
6 + error /private* "Unauthorized" 410
7 + error /hidden* "Not found" 404
8 +
9 + handle_errors 404 410 {
10 + respond "404 or 410 error"
11 + }
12 +}
13 +----------
14 +{
15 + "apps": {
16 + "http": {
17 + "http_port": 3010,
18 + "servers": {
19 + "srv0": {
20 + "listen": [
21 + ":3010"
22 + ],
23 + "routes": [
24 + {
25 + "match": [
26 + {
27 + "host": [
28 + "localhost"
29 + ]
30 + }
31 + ],
32 + "handle": [
33 + {
34 + "handler": "subroute",
35 + "routes": [
36 + {
37 + "handle": [
38 + {
39 + "handler": "vars",
40 + "root": "/srv"
41 + }
42 + ]
43 + },
44 + {
45 + "handle": [
46 + {
47 + "error": "Unauthorized",
48 + "handler": "error",
49 + "status_code": 410
50 + }
51 + ],
52 + "match": [
53 + {
54 + "path": [
55 + "/private*"
56 + ]
57 + }
58 + ]
59 + },
60 + {
61 + "handle": [
62 + {
63 + "error": "Not found",
64 + "handler": "error",
65 + "status_code": 404
66 + }
67 + ],
68 + "match": [
69 + {
70 + "path": [
71 + "/hidden*"
72 + ]
73 + }
74 + ]
75 + }
76 + ]
77 + }
78 + ],
79 + "terminal": true
80 + }
81 + ],
82 + "errors": {
83 + "routes": [
84 + {
85 + "match": [
86 + {
87 + "host": [
88 + "localhost"
89 + ]
90 + }
91 + ],
92 + "handle": [
93 + {
94 + "handler": "subroute",
95 + "routes": [
96 + {
97 + "handle": [
98 + {
99 + "body": "404 or 410 error",
100 + "handler": "static_response"
101 + }
102 + ],
103 + "match": [
104 + {
105 + "expression": "{http.error.status_code} in [404, 410]"
106 + }
107 + ]
108 + }
109 + ]
110 + }
111 + ],
112 + "terminal": true
113 + }
114 + ]
115 + }
116 + }
117 + }
118 + }
119 + }
120 +}
caddytest/integration/caddyfile_adapt/error_sort.txt new
+148
@@ -0,0 +1,148 @@
1 +{
2 + http_port 2099
3 +}
4 +localhost:2099 {
5 + root * /srv
6 + error /private* "Unauthorized" 410
7 + error /hidden* "Not found" 404
8 + error /internalerr* "Internal Server Error" 500
9 +
10 + handle_errors {
11 + respond "Fallback route: code outside the [400..499] range"
12 + }
13 + handle_errors 4xx {
14 + respond "Error in the [400 .. 499] range"
15 + }
16 +}
17 +----------
18 +{
19 + "apps": {
20 + "http": {
21 + "http_port": 2099,
22 + "servers": {
23 + "srv0": {
24 + "listen": [
25 + ":2099"
26 + ],
27 + "routes": [
28 + {
29 + "match": [
30 + {
31 + "host": [
32 + "localhost"
33 + ]
34 + }
35 + ],
36 + "handle": [
37 + {
38 + "handler": "subroute",
39 + "routes": [
40 + {
41 + "handle": [
42 + {
43 + "handler": "vars",
44 + "root": "/srv"
45 + }
46 + ]
47 + },
48 + {
49 + "handle": [
50 + {
51 + "error": "Internal Server Error",
52 + "handler": "error",
53 + "status_code": 500
54 + }
55 + ],
56 + "match": [
57 + {
58 + "path": [
59 + "/internalerr*"
60 + ]
61 + }
62 + ]
63 + },
64 + {
65 + "handle": [
66 + {
67 + "error": "Unauthorized",
68 + "handler": "error",
69 + "status_code": 410
70 + }
71 + ],
72 + "match": [
73 + {
74 + "path": [
75 + "/private*"
76 + ]
77 + }
78 + ]
79 + },
80 + {
81 + "handle": [
82 + {
83 + "error": "Not found",
84 + "handler": "error",
85 + "status_code": 404
86 + }
87 + ],
88 + "match": [
89 + {
90 + "path": [
91 + "/hidden*"
92 + ]
93 + }
94 + ]
95 + }
96 + ]
97 + }
98 + ],
99 + "terminal": true
100 + }
101 + ],
102 + "errors": {
103 + "routes": [
104 + {
105 + "match": [
106 + {
107 + "host": [
108 + "localhost"
109 + ]
110 + }
111 + ],
112 + "handle": [
113 + {
114 + "handler": "subroute",
115 + "routes": [
116 + {
117 + "handle": [
118 + {
119 + "body": "Error in the [400 .. 499] range",
120 + "handler": "static_response"
121 + }
122 + ],
123 + "match": [
124 + {
125 + "expression": "{http.error.status_code} \u003e= 400 \u0026\u0026 {http.error.status_code} \u003c= 499"
126 + }
127 + ]
128 + },
129 + {
130 + "handle": [
131 + {
132 + "body": "Fallback route: code outside the [400..499] range",
133 + "handler": "static_response"
134 + }
135 + ]
136 + }
137 + ]
138 + }
139 + ],
140 + "terminal": true
141 + }
142 + ]
143 + }
144 + }
145 + }
146 + }
147 + }
148 +}
caddytest/integration/caddyfile_test.go
+88
@@ -496,3 +496,91 @@ func TestUriReplace(t *testing.T) {
496
497 tester.AssertGetResponse("http://localhost:9080/endpoint?test={%20content%20}", 200, "test=%7B%20content%20%7D")
498 }
499 +func TestHandleErrorSimpleCodes(t *testing.T) {
500 + tester := caddytest.NewTester(t)
501 + tester.InitServer(`{
502 + admin localhost:2999
503 + http_port 9080
504 + }
505 + localhost:9080 {
506 + root * /srv
507 + error /private* "Unauthorized" 410
508 + error /hidden* "Not found" 404
509 +
510 + handle_errors 404 410 {
511 + respond "404 or 410 error"
512 + }
513 + }`, "caddyfile")
514 + // act and assert
515 + tester.AssertGetResponse("http://localhost:9080/private", 410, "404 or 410 error")
516 + tester.AssertGetResponse("http://localhost:9080/hidden", 404, "404 or 410 error")
517 +}
518 +
519 +func TestHandleErrorRange(t *testing.T) {
520 + tester := caddytest.NewTester(t)
521 + tester.InitServer(`{
522 + admin localhost:2999
523 + http_port 9080
524 + }
525 + localhost:9080 {
526 + root * /srv
527 + error /private* "Unauthorized" 410
528 + error /hidden* "Not found" 404
529 +
530 + handle_errors 4xx {
531 + respond "Error in the [400 .. 499] range"
532 + }
533 + }`, "caddyfile")
534 + // act and assert
535 + tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range")
536 + tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range")
537 +}
538 +
539 +func TestHandleErrorSort(t *testing.T) {
540 + tester := caddytest.NewTester(t)
541 + tester.InitServer(`{
542 + admin localhost:2999
543 + http_port 9080
544 + }
545 + localhost:9080 {
546 + root * /srv
547 + error /private* "Unauthorized" 410
548 + error /hidden* "Not found" 404
549 + error /internalerr* "Internal Server Error" 500
550 +
551 + handle_errors {
552 + respond "Fallback route: code outside the [400..499] range"
553 + }
554 + handle_errors 4xx {
555 + respond "Error in the [400 .. 499] range"
556 + }
557 + }`, "caddyfile")
558 + // act and assert
559 + tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Fallback route: code outside the [400..499] range")
560 + tester.AssertGetResponse("http://localhost:9080/hidden", 404, "Error in the [400 .. 499] range")
561 +}
562 +
563 +func TestHandleErrorRangeAndCodes(t *testing.T) {
564 + tester := caddytest.NewTester(t)
565 + tester.InitServer(`{
566 + admin localhost:2999
567 + http_port 9080
568 + }
569 + localhost:9080 {
570 + root * /srv
571 + error /private* "Unauthorized" 410
572 + error /threehundred* "Moved Permanently" 301
573 + error /internalerr* "Internal Server Error" 500
574 +
575 + handle_errors 500 3xx {
576 + respond "Error code is equal to 500 or in the [300..399] range"
577 + }
578 + handle_errors 4xx {
579 + respond "Error in the [400 .. 499] range"
580 + }
581 + }`, "caddyfile")
582 + // act and assert
583 + tester.AssertGetResponse("http://localhost:9080/internalerr", 500, "Error code is equal to 500 or in the [300..399] range")
584 + tester.AssertGetResponse("http://localhost:9080/threehundred", 301, "Error code is equal to 500 or in the [300..399] range")
585 + tester.AssertGetResponse("http://localhost:9080/private", 410, "Error in the [400 .. 499] range")
586 +}