
本文详解为何 Go 的 http.DetectContentType 无法准确识别 JSON MIME 类型,指出其底层基于字节嗅探(MIME sniffing)规范的局限性,并提供安全、可靠的内容类型校验中间件实现方案。
本文详解为何 go 的 `http.detectcontenttype` 无法准确识别 json mime 类型,指出其底层基于字节嗅探(mime sniffing)规范的局限性,并提供安全、可靠的内容类型校验中间件实现方案。
在 Go Web 开发中,常需通过中间件强制要求 POST/PUT 请求携带合法的 application/json Content-Type。但若直接依赖 http.DetectContentType() 判断请求体是否为 JSON,极易误判——正如问题中所示:即使客户端明确设置了 Content-Type: application/json; charset=utf-8,中间件仍返回 415 Unsupported Media Type,且日志显示 DetectContentType(buf.Bytes()) 恒为 "text/plain; charset=utf-8"。
根本原因在于:http.DetectContentType 并非解析 HTTP 头部,而是对原始字节流进行“嗅探”(sniffing),依据 WHATWG MIME Sniffing 规范推断类型。该规范专为浏览器安全设计,优先匹配 HTML、XML、JSONP 等可执行或易混淆格式;而标准 JSON 文本(如 {"key":"value"})在规范中未被定义为可明确识别的 JSON 资源,因此默认回落至 text/plain。查看 Go 源码 net/http/sniff.go 第 252 行的 match 函数即可验证:其 JSON 匹配逻辑仅覆盖极少数带 BOM 或前导空白的边缘情况,不适用于常规 JSON 载荷。
✅ 正确做法是:信任并校验 Content-Type 请求头,而非请求体字节。HTTP 协议明确规定,客户端有责任在 Content-Type 头中如实声明载荷格式,服务端应以此为准(RFC 7231 §3.1.1.5)。以下是推荐的中间件实现:
func EnforceJSON(h httprouter.Handle) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// 1. 检查请求方法(仅对 POST/PUT 等有 body 的方法校验)
if r.Method != "POST" && r.Method != "PUT" && r.Method != "PATCH" {
h(w, r, ps)
return
}
// 2. 检查 Content-Length(避免空体)
if r.ContentLength == 0 {
http.Error(w, "Request body is required", http.StatusBadRequest)
return
}
// 3. 从 Header 获取 Content-Type(关键!)
contentType := r.Header.Get("Content-Type")
if contentType == "" {
http.Error(w, "Content-Type header is missing", http.StatusUnsupportedMediaType)
return
}
// 4. 安全地解析并匹配 MIME 类型(忽略参数如 charset)
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
http.Error(w, "Invalid Content-Type format", http.StatusUnsupportedMediaType)
return
}
// 5. 允许 application/json 及其子类型(如 application/vnd.api+json)
if !strings.HasPrefix(mediaType, "application/json") {
http.Error(w, "Content-Type must be application/json or compatible", http.StatusUnsupportedMediaType)
return
}
// ✅ 校验通过,继续处理
h(w, r, ps)
}
}⚠️ 注意事项:
- 切勿读取 req.Body 后再校验:DetectContentType 需读取 body,但 req.Body 是单次读取流,一旦被中间件消费,后续处理器(如 json.Unmarshal)将读到空数据。上述方案完全避免了 body 读取。
- 使用 mime.ParseMediaType 解析头:它能正确剥离 charset=utf-8 等参数,只比对核心媒体类型,避免字符串精确匹配失败。
- 支持 JSON 变体:用 strings.HasPrefix(mediaType, "application/json") 兼容 application/vnd.myapi+json 等符合 JSON 语义的自定义类型。
- 方法粒度控制:仅对可能含 body 的方法(POST/PUT/PATCH)校验,GET/HEAD 等无需检查。
总结:内容类型校验的本质是协议契约的履行,而非字节猜测。始终以 Content-Type 请求头为权威依据,结合 mime.ParseMediaType 安全解析,既能规避 DetectContentType 的固有缺陷,又能保证中间件的健壮性与可维护性。










