规范要求 HEAD 请求和 GET 请求返回完全一样的 header, 包括 Content-Encoding、Content-Length 和 Content-Type, 否则一些检测工具(比如爬虫)会认为某个资源不存在或不正常。
那我们直接用 GET 的 handler 处理 HEAD 请求,通过判断是否是 HEAD,是 HEAD 就不写入 body 不就好了? 比如这样:
package main
import (
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/robots.txt", handleRobotsTxt)
r.HEAD("/robots.txt", handleRobotsTxt) // 显式支持 HEAD
r.Run(":8080")
}
func handleRobotsTxt(c *gin.Context) {
// 动态生成 robots.txt 内容
content := []byte("User-agent: *\\nDisallow: /private\\nDisallow: /privateb\\nDisallow: /privatec\\nDisallow: /privated\\nDisallow: /privatee\\nDisallow: /privatef\\n")
// 设置必要的 headers
c.Header("Content-Type", "text/plain; charset=utf-8")
c.Header("Content-Length", strconv.Itoa(w.Size()))
// 如果是 HEAD 请求,直接返回 200,不写 body
if c.Request.Method == "HEAD" {
c.Status(200)
return
}
// GET 请求返回实际内容
c.Data(200, "text/plain", content)
}
这段代码确实在大部分情况下会正确响应 HEAD 请求。
不过有两个问题:
r := gin.Default()
r.Use(
gzip.Gzip(gzip.DefaultCompression),
)
通过 Curl 测试下开启 gzip 的情况的响应(HEAD 请求同时启用 gzip 是正常情况)
curl -H "Accept-Encoding: gzip, deflate" -I <http://localhost:8433/robots.txt>
->
HTTP/1.1 200 OK
Content-Encoding: gzip
Content-Length: 23
Content-Type: text/plain
Vary: Accept-Encoding
X-Trace-Id: 1293b1339de807353c35e3d76f8d3e83
Date: Thu, 07 Aug 2025 03:34:25 GMT
可以看到返回 Content-Length 是不正确的(我们的测试数据远不止 23 byte - 就算是压缩后),这是因为 gzip 中间件依赖了写入的 body 来计算压缩后的内容大小。
那我们如何又写入 body 来兼容 gzip 的逻辑,又不想返回 body 给客户端呢?
这里就要用到 中间件来拦截 Writer 了。
先给代码,再讲解下为什么。
func HandleHeadMethod() gin.HandlerFunc {
return func(c *gin.Context) {
if c.Request.Method == "HEAD" {
// 复制一份 ResponseWriter 来捕获 body(不写入 body 但是会记录 body 长度)
w := &headResponseWriter{c.Writer, 0}
c.Writer = w
defer func() {
// 确保 Content-Length 被设置
// 测试下来,如果是 GET 请求,就算不设置 Content-Length,golang net/http 包也会自动设置,但是如果是 HEAD 请求,由于不会写入数据给 writer,net/http 包也就不会获取得到 length,也就不会设置 Content-Length
// 所以需要手动设置 Content-Length
if c.Writer.Header().Get("Content-Length") == "" {
c.Writer.Header().Set("Content-Length", strconv.Itoa(w.Size()))
}
}()
}
c.Next()
}
}
type headResponseWriter struct {
gin.ResponseWriter
size int
}
func (w *headResponseWriter) Write(data []byte) (int, error) {
w.size += len(data)
return len(data), nil // 实际上不写入数据
}
func (w *headResponseWriter) WriteString(s string) (int, error) {
w.size += len(s)
return len(s), nil
}
// gin gzip 中间件会使用 Size 方法来自动设置 Content-Length 头
func (w *headResponseWriter) Size() int {
return w.size
}
func (w *headResponseWriter) Written() bool {
return w.size > 0
}
代码中有几个关键点
// 在 github.com/gin-contrib/gzip 包里面有这个方法:
defer func() {
gz.Close()
c.Header("Content-Length", fmt.Sprint(c.Writer.Size()))
}()
确保 Content-Length 被设置
一般情况(GET)会写入 body,所以只要有 body,就算不设置 Content-Length,golang net/http 包也会自动根据 body 长度自动计算出 Content-Length 并添加到 header 中,但是如果是 HEAD 请求,我们不会写入真实数据给 writer,net/http 包也就无法获取得 length 来自动设置 Content-Length。
所以我们需要手动设置 Content-Length.
现在我们就可以更简单的方式来处理 HEAD 了:
package main
import (
"github.com/gin-gonic/gin"
"github.com/gin-contrib/gzip"
)
func main() {
r := gin.Default()
r.Use(
HandleHeadMethod(),
gzip.Gzip(gzip.DefaultCompression),
)
// 使用 Match 来同时处理多种方法的请求
r.Match([]string{"GET", "HEAD"}, "/robots.txt",
r.Run(":8080")
}
func handleRobotsTxt(c *gin.Context) {
// 动态生成 robots.txt 内容
content := []byte("User-agent: *\\nDisallow: /private\\n")
ctx.Data(200, "text/plain", []byte(robotsTxt))
}
© 2025 bysir's Blog
👋 觉得这个模板不错?
我也做一个