用户把 /Users/Profile 写成 /users/profile,Gin 默认给 404。打开 RedirectFixedPath 能帮他们改过来,代价是什么?这篇用 10 个写错的 URL 看修正规则,再测打开之后命中和 404 的耗时差别。
=== RedirectFixedPath = false ===
GET /USERS/Profile → 404
GET /hello → 404
GET /users//profile → 404
GET /users/./profile → 404
GET /users/x/../profile → 404
GET //users/profile → 404
GET /Files/ABC → 404
GET /DIR → 404
GET /DIR/ → 404
GET /users/PROFILE/ → 404
=== RedirectFixedPath = true ===
GET /USERS/Profile → 301 Location: /users/profile
GET /hello → 301 Location: /Hello
GET /users//profile → 301 Location: /users/profile
GET /users/./profile → 301 Location: /users/profile
GET /users/x/../profile → 301 Location: /users/profile
GET //users/profile → 301 Location: /users/profile
GET /Files/ABC → 301 Location: /files/ABC
GET /DIR → 301 Location: /dir/
GET /DIR/ → 301 Location: /dir/
GET /users/PROFILE/ → 301 Location: /users/profile大小写会被改成路由注册时的写法(/hello → /Hello);重复斜杠、./、../ 被 cleanPath 化简;结尾斜杠也一并处理。参数的值不会被改动:/Files/ABC 只把静态的 Files 改成 files,ABC 保持原样。
注意修正后的 301 是给客户端的,浏览器地址栏会变成规范路径。
gin.go 的 redirectFixedPath(Gin v1.12.0):
808 | func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
809 | req := c.Request
810 | rPath := req.URL.Path
811 |
812 | if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(rPath), trailingSlash); ok {
813 | req.URL.Path = bytesconv.BytesToString(fixedPath)
814 | redirectRequest(c)
815 | return true
816 | }
817 | return false
818 | }
gin.go: 106 | // RedirectFixedPath if enabled, the router tries to fix the current request path, if no
gin.go: 115 | RedirectFixedPath bool
gin.go: 196 | // - RedirectFixedPath: false
gin.go: 212 | RedirectFixedPath: false,
gin.go: 731 | if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
POST /api/items → 307 Location: /Api/Items
POST /api//items → 307 Location: /Api/Items
GET /api/items → 404
--- 同时命中 TSR(少一个斜杠)和大小写错误 ---
GET /docs → 301 Location: /Docs/
GET /DOCS/ → 301 Location: /Docs/实现就是 redirectFixedPath 里的几行:先 cleanPath,再在这个方法的树里做不区分大小写的查找(并把 RedirectFixedPath 自己当作“是否顺便修正结尾斜杠”传进去),成功就走 redirectRequest。所以状态码规则与上一篇相同:GET 301,其他方法 307。查找只在当前方法的树里进行,GET 请求不会被修正到一条 POST 路由。
第 1 轮
fixed=false 命中 /api/v1/resource500/items/9 中位 38.5 ns/op(最小 38.4 / 最大 38.7),0 allocs/op
fixed=false 404 /nothing/here 中位 35.7 ns/op(最小 35.5 / 最大 36.2),0 allocs/op
fixed=false 404 /api/v1/resource500/other 中位 49.4 ns/op(最小 49.3 / 最大 49.9),0 allocs/op
fixed=true 命中 /api/v1/resource500/items/9 中位 38.4 ns/op(最小 38.2 / 最大 38.5),0 allocs/op
fixed=true 404 /nothing/here 中位 137.7 ns/op(最小 137.0 / 最大 139.4),1 allocs/op
fixed=true 404 /api/v1/resource500/other 中位 163.0 ns/op(最小 162.2 / 最大 164.7),1 allocs/op
第 2 轮
fixed=false 命中 /api/v1/resource500/items/9 中位 38.3 ns/op(最小 38.2 / 最大 38.5),0 allocs/op
fixed=false 404 /nothing/here 中位 35.9 ns/op(最小 35.6 / 最大 36.5),0 allocs/op
fixed=false 404 /api/v1/resource500/other 中位 49.5 ns/op(最小 48.8 / 最大 49.8),0 allocs/op
fixed=true 命中 /api/v1/resource500/items/9 中位 38.3 ns/op(最小 38.0 / 最大 38.5),0 allocs/op
fixed=true 404 /nothing/here 中位 137.2 ns/op(最小 135.6 / 最大 138.2),1 allocs/op
fixed=true 404 /api/v1/resource500/other 中位 162.9 ns/op(最小 159.9 / 最大 163.8),1 allocs/op命中的请求根本不会走到这段代码,耗时不变。每个不存在的路径却要多做一次 cleanPath 和一次不区分大小写的树遍历(并且多一次内存分配),在 1000 条路由的树里,404 从约 36 ns 涨到约 137 ns。对正常流量没有影响;如果会被扫描器、爬虫大量打 404,这点开销才值得注意。