bigywater.tech
Gin · 11

RedirectFixedPath:大小写与多余斜杠怎么修正

用户把 /Users/Profile 写成 /users/profile,Gin 默认给 404。打开 RedirectFixedPath 能帮他们改过来,代价是什么?这篇用 10 个写错的 URL 看修正规则,再测打开之后命中和 404 的耗时差别。

10 / 10
打开 RedirectFixedPath 后被 301 修正的错误写法个数(关闭时 0 个)
36 → 137 ns
/nothing/here 这类 404 请求的耗时
38.3 = 38.3 ns
命中路由的耗时,打开前后相同

能修什么

实测 · 本机执行输出
    === 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 路由。

代价只落在 404 上

实测 · 本机执行输出
    第 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,这点开销才值得注意。

分步演示

路径修正
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。前两节用 httptest 直接调用 ServeHTTP;基准是对 Engine.ServeHTTP 加一个丢弃输出的 ResponseWriter 的 testing.Benchmark,9 轮取中位数,整个实验跑了两轮;每种路由表只有 GET
    • 没有验证:非 ASCII(中文、带重音字母)路径的大小写折叠;路由树更大(上万条)时 404 的耗时;与 RedirectTrailingSlash 都关掉/都打开的所有组合;真实浏览器对这类 301 的缓存
    京ICP备2026067502号-1京公网安备11010502063487号