bigywater.tech
Gin · 40

Negotiate:按 Accept 选择响应格式

Accept 头里有 q 值、通配符、大小写不一样,Gin 的 Negotiate 到底怎么选?这篇从 NegotiateFormat 的 20 行源码出发,用 16 种 Accept 头把它的真实行为列出来,看它与 RFC 的差别和使用时的坑。

q=0 仍选中
Accept: application/json;q=0, text/html 选出的是 application/json
406
Offered 写 application/x-yaml 时,Accept: application/yaml 匹配不上
TEXT/HTML → 空
Accept 大小写不一致就匹配不上任何 Offered

实现只有二十行

实测 · 本机执行输出
  utils.go 的 parseAccept(Gin v1.12.0):
     110 | func parseAccept(acceptHeader string) []string {
     111 |     parts := strings.Split(acceptHeader, ",")
     112 |     out := make([]string, 0, len(parts))
     113 |     for _, part := range parts {
     114 |         if i := strings.IndexByte(part, ';'); i > 0 {
     115 |             part = part[:i]
     116 |         }
     117 |         if part = strings.TrimSpace(part); part != "" {
     118 |             out = append(out, part)
     119 |         }
     120 |     }
     121 |     return out
     122 | }
  context.go 的 NegotiateFormat(Gin v1.12.0):
    1394 | func (c *Context) NegotiateFormat(offered ...string) string {
    1395 |     assert1(len(offered) > 0, "you must provide at least one offer")
    1396 | 
    1397 |     if c.Accepted == nil {
    1398 |         c.Accepted = parseAccept(c.requestHeader("Accept"))
    1399 |     }
    1400 |     if len(c.Accepted) == 0 {
    1401 |         return offered[0]
    1402 |     }
    1403 |     for _, accepted := range c.Accepted {
    1404 |         for _, offer := range offered {
    1405 |             // According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
    1406 |             // therefore we can just iterate over the string without casting it into []rune
    1407 |             i := 0
    1408 |             for ; i < len(accepted) && i < len(offer); i++ {
    1409 |                 if accepted[i] == '*' || offer[i] == '*' {
    1410 |                     return offer
    1411 |                 }
    1412 |                 if accepted[i] != offer[i] {
    1413 |                     break
    1414 |                 }
    1415 |             }
    1416 |             if i == len(accepted) {
    1417 |                 return offer
    1418 |             }
    1419 |         }
    1420 |     }
    1421 |     return ""
    1422 | }

Gin 先把 Accept 按逗号拆开,把每一项分号后面的参数(包括 q=)整体丢掉,然后按出现顺序逐项去匹配 Offered,逐字符比较前缀,遇到 * 就算命中。所以它不是 RFC 7231 意义上的“按质量值协商”,而是“按 Accept 书写顺序的首个匹配”。

16 种 Accept 头

实测 · 本机执行输出
    Accept: "<无>"                                          → "application/json"
    Accept: ""                                             → "application/json"
    Accept: "application/json"                             → "application/json"
    Accept: "application/xml"                              → "application/xml"
    Accept: "text/html"                                    → "text/html"
    Accept: "text/html,application/xml;q=0.9,*/*;q=0.8"    → "text/html"
    Accept: "text/html;q=0.1, application/json;q=0.9"      → "text/html"
    Accept: "application/json;q=0, text/html"              → "application/json"
    Accept: "*/*"                                          → "application/json"
    Accept: "application/*"                                → "application/json"
    Accept: "text/*"                                       → "text/html"
    Accept: "image/png"                                    → ""
    Accept: "image/png, */*"                               → "application/json"
    Accept: "TEXT/HTML"                                    → ""
    Accept: "application/vnd.api+json"                     → ""
    Accept: "application/json, text/html"                  → "application/json"

浏览器式的 text/html,application/xml;q=0.9,*/*;q=0.8 选 HTML;text/html;q=0.1, application/json;q=0.9 仍选 HTML(客户端其实更想要 JSON);application/json;q=0, text/html 选了客户端明确拒绝的 JSON。通配符 */* 选第一个 Offered,所以 Offered 的第一个应该放最通用的格式。TEXT/HTML 大写、application/vnd.api+json 这样的子类型后缀都匹配不上。

Negotiate 渲染

实测 · 本机执行输出
    Accept: "application/json"         → 200 Content-Type="application/json; charset=utf-8"  "{\"name\":\"ann\",\"age\":30}"
    Accept: "application/xml"          → 200 Content-Type="application/xml; charset=utf-8"   "<U><name>ann</name><age>30</age></U>"
    Accept: "application/x-yaml"       → 200 Content-Type="application/yaml; charset=utf-8"  "name: ann\nage: 30"
    Accept: "application/yaml"         → 406 Content-Type=""                                 ""
    Accept: "text/html"                → 406 Content-Type=""                                 ""
    Accept: "text/html,application/xml" → 200 Content-Type="application/xml; charset=utf-8"   "<U><name>ann</name><age>30</age></U>"
    Accept: "<无>"                      → 200 Content-Type="application/json; charset=utf-8"  "{\"name\":\"ann\",\"age\":30}"
    Accept: "*/*"                      → 200 Content-Type="application/json; charset=utf-8"  "{\"name\":\"ann\",\"age\":30}"
    /data Accept: "application/json" → 200 "{\"only\":\"给 JSON 的数据\"}"
    /data Accept: "application/xml"  → 200 "<string>通用数据</string>"

c.Negotiate 用选出的格式去调用对应的渲染方法。Data 是通用数据,JSONData、XMLData 等可以给个别格式提供不同的数据。有一个容易踩的坑:YAML 的两个 MIME(application/x-yaml 与 application/yaml)只有你写进 Offered 的那个能匹配上,而渲染出的 Content-Type 总是 application/yaml。

406 与边界

实测 · 本机执行输出
  context.go 的 Negotiate 末尾(Gin v1.12.0):
    1388 |     default:
    1389 |         c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
    1390 |     }
    1391 | }
    1392 | 
    Accept: text/html → 状态 406 正文 "" X-Before="1" Content-Type=""
     第一次 "";同一请求里再用另一组 Offered "application/xml";c.Accepted=[text/html application/xml]
     panic: you must provide at least one offer

没有任何格式匹配时,Negotiate 调用 AbortWithError(406, …),错误信息是 “the accepted formats are not offered by the server”,同时记入 c.Errors;响应没有正文和 Content-Type。NegotiateFormat 自己只返回空串,由你决定怎么处理。Accept 在一个请求内只解析一次(缓存在 c.Accepted)。

分步演示

内容协商
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。请求用 httptest 直接调用 ServeHTTP,Accept 头逐个设置;每种情形各一次
    • 没有验证:HTML 格式的 Negotiate(需要模板,见模板一篇);XML / YAML 在带类型的结构体上的输出细节(只试了一个简单结构体);与 RFC 7231 的完整对照(只列出了差别);浏览器在真实请求里发送的 Accept 头(这里用的是手写的典型值);Protobuf、BSON、TOML 格式的协商
    京ICP备2026067502号-1京公网安备11010502063487号