Accept 头里有 q 值、通配符、大小写不一样,Gin 的 Negotiate 到底怎么选?这篇从 NegotiateFormat 的 20 行源码出发,用 16 种 Accept 头把它的真实行为列出来,看它与 RFC 的差别和使用时的坑。
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 书写顺序的首个匹配”。
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 这样的子类型后缀都匹配不上。
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。
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)。