Skip to content

Commit 5622ecc

Browse files
aldaslammel
andauthored
Fix performance regression caused by path escaping (#1777, #1798, #1799)
* Fix performance regression #1777 and avoid double escaping in rewrite/proxy middleware. * Add rewrite test for correct escaping of replacement (#1798) Co-authored-by: Roland Lammel <[email protected]>
1 parent cffd3ef commit 5622ecc

File tree

6 files changed

+376
-85
lines changed

6 files changed

+376
-85
lines changed

echo.go

+14-2
Original file line numberDiff line numberDiff line change
@@ -629,12 +629,12 @@ func (e *Echo) ServeHTTP(w http.ResponseWriter, r *http.Request) {
629629
h := NotFoundHandler
630630

631631
if e.premiddleware == nil {
632-
e.findRouter(r.Host).Find(r.Method, r.URL.EscapedPath(), c)
632+
e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
633633
h = c.Handler()
634634
h = applyMiddleware(h, e.middleware...)
635635
} else {
636636
h = func(c Context) error {
637-
e.findRouter(r.Host).Find(r.Method, r.URL.EscapedPath(), c)
637+
e.findRouter(r.Host).Find(r.Method, GetPath(r), c)
638638
h := c.Handler()
639639
h = applyMiddleware(h, e.middleware...)
640640
return h(c)
@@ -909,6 +909,18 @@ func WrapMiddleware(m func(http.Handler) http.Handler) MiddlewareFunc {
909909
}
910910
}
911911

912+
// GetPath returns RawPath, if it's empty returns Path from URL
913+
// Difference between RawPath and Path is:
914+
// * Path is where request path is stored. Value is stored in decoded form: /%47%6f%2f becomes /Go/.
915+
// * RawPath is an optional field which only gets set if the default encoding is different from Path.
916+
func GetPath(r *http.Request) string {
917+
path := r.URL.RawPath
918+
if path == "" {
919+
path = r.URL.Path
920+
}
921+
return path
922+
}
923+
912924
func (e *Echo) findRouter(host string) *Router {
913925
if len(e.routers) > 0 {
914926
if r, ok := e.routers[host]; ok {

echo_test.go

+83-6
Original file line numberDiff line numberDiff line change
@@ -468,15 +468,46 @@ func TestEchoRoutes(t *testing.T) {
468468
}
469469
}
470470

471-
func TestEchoEncodedPath(t *testing.T) {
471+
func TestEchoServeHTTPPathEncoding(t *testing.T) {
472472
e := New()
473+
e.GET("/with/slash", func(c Context) error {
474+
return c.String(http.StatusOK, "/with/slash")
475+
})
473476
e.GET("/:id", func(c Context) error {
474-
return c.NoContent(http.StatusOK)
477+
return c.String(http.StatusOK, c.Param("id"))
475478
})
476-
req := httptest.NewRequest(http.MethodGet, "/with%2Fslash", nil)
477-
rec := httptest.NewRecorder()
478-
e.ServeHTTP(rec, req)
479-
assert.Equal(t, http.StatusOK, rec.Code)
479+
480+
var testCases = []struct {
481+
name string
482+
whenURL string
483+
expectURL string
484+
expectStatus int
485+
}{
486+
{
487+
name: "url with encoding is not decoded for routing",
488+
whenURL: "/with%2Fslash",
489+
expectURL: "with%2Fslash", // `%2F` is not decoded to `/` for routing
490+
expectStatus: http.StatusOK,
491+
},
492+
{
493+
name: "url without encoding is used as is",
494+
whenURL: "/with/slash",
495+
expectURL: "/with/slash",
496+
expectStatus: http.StatusOK,
497+
},
498+
}
499+
500+
for _, tc := range testCases {
501+
t.Run(tc.name, func(t *testing.T) {
502+
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
503+
rec := httptest.NewRecorder()
504+
505+
e.ServeHTTP(rec, req)
506+
507+
assert.Equal(t, tc.expectStatus, rec.Code)
508+
assert.Equal(t, tc.expectURL, rec.Body.String())
509+
})
510+
}
480511
}
481512

482513
func TestEchoGroup(t *testing.T) {
@@ -1211,3 +1242,49 @@ func TestEcho_StartServer(t *testing.T) {
12111242
})
12121243
}
12131244
}
1245+
1246+
func benchmarkEchoRoutes(b *testing.B, routes []*Route) {
1247+
e := New()
1248+
req := httptest.NewRequest("GET", "/", nil)
1249+
u := req.URL
1250+
w := httptest.NewRecorder()
1251+
1252+
b.ReportAllocs()
1253+
1254+
// Add routes
1255+
for _, route := range routes {
1256+
e.Add(route.Method, route.Path, func(c Context) error {
1257+
return nil
1258+
})
1259+
}
1260+
1261+
// Find routes
1262+
b.ResetTimer()
1263+
for i := 0; i < b.N; i++ {
1264+
for _, route := range routes {
1265+
req.Method = route.Method
1266+
u.Path = route.Path
1267+
e.ServeHTTP(w, req)
1268+
}
1269+
}
1270+
}
1271+
1272+
func BenchmarkEchoStaticRoutes(b *testing.B) {
1273+
benchmarkEchoRoutes(b, staticRoutes)
1274+
}
1275+
1276+
func BenchmarkEchoStaticRoutesMisses(b *testing.B) {
1277+
benchmarkEchoRoutes(b, staticRoutes)
1278+
}
1279+
1280+
func BenchmarkEchoGitHubAPI(b *testing.B) {
1281+
benchmarkEchoRoutes(b, gitHubAPI)
1282+
}
1283+
1284+
func BenchmarkEchoGitHubAPIMisses(b *testing.B) {
1285+
benchmarkEchoRoutes(b, gitHubAPI)
1286+
}
1287+
1288+
func BenchmarkEchoParseAPI(b *testing.B) {
1289+
benchmarkEchoRoutes(b, parseAPI)
1290+
}

middleware/middleware.go

+21-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package middleware
22

33
import (
44
"net/http"
5+
"net/url"
56
"regexp"
67
"strconv"
78
"strings"
@@ -50,10 +51,26 @@ func rewriteRulesRegex(rewrite map[string]string) map[*regexp.Regexp]string {
5051

5152
func rewritePath(rewriteRegex map[*regexp.Regexp]string, req *http.Request) {
5253
for k, v := range rewriteRegex {
53-
replacerRawPath := captureTokens(k, req.URL.EscapedPath())
54-
if replacerRawPath != nil {
55-
replacerPath := captureTokens(k, req.URL.Path)
56-
req.URL.RawPath, req.URL.Path = replacerRawPath.Replace(v), replacerPath.Replace(v)
54+
rawPath := req.URL.RawPath
55+
if rawPath != "" {
56+
// RawPath is only set when there has been escaping done. In that case Path must be deduced from rewritten RawPath
57+
// because encoded Path could match rules that RawPath did not
58+
if replacer := captureTokens(k, rawPath); replacer != nil {
59+
rawPath = replacer.Replace(v)
60+
61+
req.URL.RawPath = rawPath
62+
req.URL.Path, _ = url.PathUnescape(rawPath)
63+
64+
return // rewrite only once
65+
}
66+
67+
continue
68+
}
69+
70+
if replacer := captureTokens(k, req.URL.Path); replacer != nil {
71+
req.URL.Path = replacer.Replace(v)
72+
73+
return // rewrite only once
5774
}
5875
}
5976
}

middleware/middleware_test.go

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package middleware
2+
3+
import (
4+
"github.com/stretchr/testify/assert"
5+
"net/http"
6+
"net/http/httptest"
7+
"regexp"
8+
"testing"
9+
)
10+
11+
func TestRewritePath(t *testing.T) {
12+
var testCases = []struct {
13+
whenURL string
14+
expectPath string
15+
expectRawPath string
16+
}{
17+
{
18+
whenURL: "http://localhost:8080/old",
19+
expectPath: "/new",
20+
expectRawPath: "",
21+
},
22+
{ // encoded `ol%64` (decoded `old`) should not be rewritten to `/new`
23+
whenURL: "/ol%64", // `%64` is decoded `d`
24+
expectPath: "/old",
25+
expectRawPath: "/ol%64",
26+
},
27+
{
28+
whenURL: "http://localhost:8080/users/+_+/orders/___++++?test=1",
29+
expectPath: "/user/+_+/order/___++++",
30+
expectRawPath: "",
31+
},
32+
{
33+
whenURL: "http://localhost:8080/users/%20a/orders/%20aa",
34+
expectPath: "/user/ a/order/ aa",
35+
expectRawPath: "",
36+
},
37+
{
38+
whenURL: "http://localhost:8080/%47%6f%2f",
39+
expectPath: "/Go/",
40+
expectRawPath: "/%47%6f%2f",
41+
},
42+
{
43+
whenURL: "/users/jill/orders/T%2FcO4lW%2Ft%2FVp%2F",
44+
expectPath: "/user/jill/order/T/cO4lW/t/Vp/",
45+
expectRawPath: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F",
46+
},
47+
{ // do nothing, replace nothing
48+
whenURL: "http://localhost:8080/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F",
49+
expectPath: "/user/jill/order/T/cO4lW/t/Vp/",
50+
expectRawPath: "/user/jill/order/T%2FcO4lW%2Ft%2FVp%2F",
51+
},
52+
}
53+
54+
rules := map[*regexp.Regexp]string{
55+
regexp.MustCompile("^/old$"): "/new",
56+
regexp.MustCompile("^/users/(.*?)/orders/(.*?)$"): "/user/$1/order/$2",
57+
}
58+
59+
for _, tc := range testCases {
60+
t.Run(tc.whenURL, func(t *testing.T) {
61+
req := httptest.NewRequest(http.MethodGet, tc.whenURL, nil)
62+
63+
rewritePath(rules, req)
64+
65+
assert.Equal(t, tc.expectPath, req.URL.Path) // Path field is stored in decoded form: /%47%6f%2f becomes /Go/.
66+
assert.Equal(t, tc.expectRawPath, req.URL.RawPath) // RawPath, an optional field which only gets set if the default encoding is different from Path.
67+
})
68+
}
69+
}

0 commit comments

Comments
 (0)