# Golang Gin 如何正确处理 HEAD 请求

08-07 / 2025

规范要求 HEAD 请求和 GET 请求返回完全一样的 header, 包括 Content-Encoding、Content-Length 和 Content-Type, 否则一些检测工具（比如爬虫）会认为某个资源不存在或不正常。

那我们直接用 GET 的 handler 处理 HEAD 请求，通过判断是否是 HEAD，是 HEAD 就不写入 body 不就好了？比如这样：

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
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\n")

	// 设置必要的 headers
	c.Header("Content-Type", "text/plain; charset=utf-8")

	// 如果是 HEAD 请求，直接返回 200，不写 body
	if c.Request.Method == "HEAD" {
		c.Status(200)
		return
	}

	// GET 请求返回实际内容
	c.Data(200, "text/plain", content)
}

```

这段代码确实在大部分情况下会正确响应 HEAD 请求。

**不过有两个问题**：

- 这种写法会入侵到所有业务代码、每个 Handler，并不优雅
- **如果你同时还用了 gzip 中间件，则会出现 Content-Length 不正确的问题。**

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
r := gin.Default()

r.Use(
	gzip.Gzip(gzip.DefaultCompression),
)

```

通过 Curl 测试下开启 gzip 的情况的响应（HEAD 请求同时启用 gzip 是正常情况）

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
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 了。

先给代码，再讲解下为什么。

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
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，所以需要手动设置
				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 逻辑

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
// 在 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 请求

```rounded bg-ink/[0.06] px-1.5 py-0.5 font-monocn text-[0.88em] text-ink
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", handleRobotsTxt)

	r.Run(":8080")
}

func handleRobotsTxt(c *gin.Context) {
	// 动态生成 robots.txt 内容
	content := []byte("User-agent: *\nDisallow: /private\n")

	c.Data(200, "text/plain", content)
}

```

> 全站页面清单：[/llms.txt](/llms.txt)
