bigywater.tech
Gin · 10

RedirectTrailingSlash:301 还是 307

/dir 和 /dir/ 在 Gin 里是两条不同的路由,默认会互相重定向。这篇用状态码和 Location 把规则列清楚:什么时候是 301、什么时候是 307、query 和反向代理的前缀怎么处理,以及真实的 HTTP 客户端遇到这种重定向会怎样。

301 / 307
GET / 其他方法的重定向状态码
1 次
307 重定向后 POST /upload/ 的 handler 被调用次数,body 不丢
/evilcom/dir/
X-Forwarded-Prefix 为 //evil.com 时的 Location

默认就开着

实测 · 本机执行输出
    默认值 RedirectTrailingSlash = true ,RedirectFixedPath = false ,HandleMethodNotAllowed = false
    GET    /dir                   → 301  Location: /dir/
    GET    /dir/                  → 200
    GET    /file                  → 200
    GET    /file/                 → 301  Location: /file
    GET    /users/7               → 301  Location: /users/7/
    GET    /users/7/              → 200
    GET    /dir?x=1&y=2           → 301  Location: /dir/?x=1&y=2
    GET    /file/?x=1             → 301  Location: /file?x=1

Engine 的默认值是 RedirectTrailingSlash=true,RedirectFixedPath=false,HandleMethodNotAllowed=false。注册 /dir/ 之后请求 /dir 会 301 到 /dir/,反过来 /file 注册了就请求 /file/ 则 301 去掉斜杠。参数路由同样适用,query 保持原样。

301 还是 307

实测 · 本机执行输出
  gin.go 的 redirectRequest(Gin v1.12.0):
     820 | func redirectRequest(c *Context) {
     821 |     req := c.Request
     822 |     rPath := req.URL.Path
     823 |     rURL := req.URL.String()
     824 | 
     825 |     code := http.StatusMovedPermanently // Permanent redirect, request with GET method
     826 |     if req.Method != http.MethodGet {
     827 |         code = http.StatusTemporaryRedirect
     828 |     }
     829 |     debugPrint("redirecting request %d: %s --> %s", code, rPath, rURL)
     830 |     http.Redirect(c.Writer, req, rURL, code)
     831 |     c.writermem.WriteHeaderNow()
     832 | }
    GET    /x                     → 301  Location: /x/
    HEAD   /x                     → 307  Location: /x/
    POST   /x                     → 307  Location: /x/
    PUT    /x                     → 307  Location: /x/
    PATCH  /x                     → 307  Location: /x/
    DELETE /x                     → 307  Location: /x/
    OPTIONS /x                     → 307  Location: /x/
    CONNECT /x                     → 404

状态码只由方法决定:GET 用 301,其他方法用 307。这也包括 HEAD——HEAD 拿到的是 307。307 要求客户端用相同方法和 body 重发,所以 POST 到 /upload、而路由注册成 /upload/ 时,不会像 302 那样被降级成 GET。CONNECT 完全不做重定向。

关闭与反向代理

实测 · 本机执行输出
    GET    /dir                   → 301  Location: /dir/
    --- RedirectTrailingSlash = false ---
    GET    /dir                   → 404
    --- 带 X-Forwarded-Prefix 头(反向代理挂在子路径下)---
    GET    /dir                   [X-Forwarded-Prefix: "/app"] → 301  Location: /app/dir/
    GET    /dir                   [X-Forwarded-Prefix: "/app/"] → 301  Location: /app/dir/
    GET    /dir                   [X-Forwarded-Prefix: "//evil.com"] → 301  Location: /evilcom/dir/
    GET    /dir                   [X-Forwarded-Prefix: "/a b;c"] → 301  Location: /abc/dir/

关掉 RedirectTrailingSlash 后,结尾斜杠不一致就是普通的 404。挂在反向代理子路径下时,代理设置的 X-Forwarded-Prefix 会拼进 Location,Gin 先用 sanitizePathChars 只保留字母、数字、斜杠和连字符,所以带 . 的 //evil.com 变成 /evilcom/,不会产生开放重定向。

真实客户端

实测 · 本机执行输出
    客户端被重定向到 /upload/,方法 POST
    最终响应:200 "stored"
    服务端的 handler 看到:[POST /upload/ body="hello"]
    不跟随重定向时:301,Location=/old/

Go 的 http.Client 遇到 307 会用同一方法重发,body 也被带上:服务端 handler 只被调用一次,拿到完整的 hello。GET 的 301 则被客户端当作普通重定向。是否缓存 301、其他语言客户端对 307 的处理不一定相同,本文没有验证。

分步演示

尾斜杠重定向
本步的对象

    本文用到的工具与边界

    • 本机 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,第四节用 httptest.NewServer 起真实的本机 HTTP 服务和 Go 的 http.Client;每种情形各一次
    • 没有验证:浏览器对 301 的永久缓存行为;curl、Python requests、浏览器对 307 + POST 的处理;HTTP/2 下的行为;非 ASCII 路径的重定向 Location 编码;RemoveExtraSlash 与 RedirectTrailingSlash 同时打开时的交互(下一篇与后面的文章再看)
    京ICP备2026067502号-1京公网安备11010502063487号