Golang Gin 如何正确处理 HEAD 请求

08-07 / 2025

规范要求 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 请求。

不过有两个问题

  • 这种写法会入侵到所有业务代码、每个 Handler,并不优雅
  • 如果你同时还用了 gzip 中间件,则会出现 Content-Length 不正确的问题。
	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
}

代码中有几个关键点

  • 需要实现 Size() 方法来兼容 gzip 的写入 Content-Length 逻辑
  // 在 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 了:

  • 在 Gzip 之前使用 HandleHeadMethod 中间件
  • 业务代码无需关心是否是 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

👋 觉得这个模板不错?

我也做一个