如何在 Go 中正确验证 HTTP 请求的 JSON MIME 类型

1次阅读

如何在 Go 中正确验证 HTTP 请求的 JSON MIME 类型

本文详解为何 http.DetectContentType 无法可靠识别 application/json,揭示其底层 MIME 探测机制的局限性,并提供安全、标准的替代方案——直接解析 Content-Type 请求头。

本文详解为何 `http.detectcontenttype` 无法可靠识别 `application/json`,揭示其底层 mime 探测机制的局限性,并提供安全、标准的替代方案——直接解析 `content-type` 请求头。

go Web 开发中,中间件常用于统一校验请求格式(如强制要求 json)。但若误用 http.DetectContentType 判断 JSON 类型,极易导致“明明设置了 Content-Type: application/json,却始终返回 415 Unsupported Media Type”的问题——这正是因该函数不基于请求头,而仅依据请求体字节特征进行启发式探测,且根据 WHATWG MIME Sniffing 规范,JSON 内容(如 {“key”:”value”})会被归类为 text/plain,而非 application/json。

❌ 错误做法:依赖 DetectContentType 判断 JSON

func EnforceJSON(h httprouter.Handle) httprouter.Handle {     return func(rw http.ResponseWriter, req *http.Request, ps httprouter.Params) {         if req.ContentLength == 0 {             http.Error(rw, "Empty body", http.StatusBadRequest)             return         }          buf := new(bytes.Buffer)         buf.ReadFrom(req.Body) // ⚠️ 此操作已消耗 req.Body!后续 handler 将读不到数据         detected := http.DetectContentType(buf.Bytes())         fmt.Println("Detected:", detected) // 总是输出 "text/plain; charset=utf-8"          if detected != "application/json; charset=utf-8" {             http.Error(rw, "Media type not supported", http.StatusUnsupportedMediaType)             return         }          h(rw, req, ps) // ❌ req.Body 已为空!     } }

该实现存在两个严重缺陷

  • DetectContentType 无法识别 JSON(规范未定义 JSON 签名规则),仅能识别 PNG、JPEG、xml 等有明确字节前缀的格式;
  • buf.ReadFrom(req.Body) 永久消耗了请求体流,导致下游处理器(如 json.Decode(req.Body))读取空内容,引发解码失败。

✅ 正确做法:严格校验 Content-Type 请求头

MIME 类型应由客户端通过 Content-Type 头明确声明,服务端职责是验证该声明是否符合预期,而非“猜测”内容类型。标准做法如下:

import (     "net/http"     "strings" )  func EnforceJSON(h httprouter.Handle) httprouter.Handle {     return func(rw http.ResponseWriter, req *http.Request, ps httprouter.Params) {         // 1. 检查请求体是否存在         if req.ContentLength == 0 {             http.Error(rw, "Request body is required", http.StatusBadRequest)             return         }          // 2. 从 Header 获取 Content-Type(忽略大小写和参数,如 charset)         contentType := req.Header.Get("Content-Type")         if contentType == "" {             http.Error(rw, "Content-Type header is missing", http.StatusUnsupportedMediaType)             return         }          // 3. 标准化并匹配:支持 application/json、application/json;charset=utf-8 等         mediaType := strings.TrimSpace(strings.Split(contentType, ";")[0])         if mediaType != "application/json" {             http.Error(rw, "Content-Type must be application/json", http.StatusUnsupportedMediaType)             return         }          // 4. ✅ 关键:重置 Body(若需多次读取)或确保下游可读         // (此处无需重置,因为 Body 未被消费;后续 handler 可直接使用 req.Body)         h(rw, req, ps)     } }

? 补充说明与最佳实践

  • 为什么不用 DetectContentType?
    Go 的 http.DetectContentType 实现严格遵循 WHATWG MIME Sniffing 规范,其第 7 节明确指出:JSON 不在可探测类型列表中。它仅对二进制格式(如 PNG 的 x89PNG)或 XML/HTML 的特定开头做匹配,对纯文本 JSON 永远返回 text/plain。

  • charset 参数是否必须检查?
    不必。application/json 规范(RFC 8259)规定其编码必须为 UTF-8,且 charset 参数在 JSON 中是冗余甚至错误的。生产环境建议忽略 charset,只校验主媒体类型。

  • 需要重用 Body 吗?
    如果中间件需读取 Body(如日志、鉴权),请用 http.MaxBytesReader 包装或使用 io.NopCloser(bytes.NewReader(buf.Bytes())) 重建 Body,避免影响下游逻辑。

  • 增强健壮性(可选)
    可结合 mime.ParseMediaType 解析完整头信息,或添加 Accept: application/json 响应头提示客户端:

    rw.Header().Set("Content-Type", "application/json; charset=utf-8")

遵循“声明即契约”的原则,以 Content-Type 头为准,既符合 HTTP 语义,又规避了探测机制的不可靠性。这是构建可维护、可测试 Go Web 中间件的基石实践。

text=ZqhQzanResources