📘 第六章 HTTP首部
📄 一句话概括

Accept 告诉服务端「我能看什么格式」,用 q 值排优先级。 它是最典型的内容协商字段:同一份数据,可以有多种交付形式。

📝 本节和前面几节的关系

通用首部讲完了,从这一节开始是 19 个请求首部。第一个就是协商家族的老大。

一、q 值怎么读

值是一串媒体类型,用逗号分隔,每个后面可以跟 ;q=0.x。q 是 0 到 1 之间的权重,越大越想要。 text/html, application/json;q=0.5 读作「HTML 最好(默认 q=1),JSON 也能凑合」。 Demo 里六组请求,服务端每次都按 q 值排序挑第一个。 q 只到小数点后三位。 写 q=0.1234 是非法的,多出来的位会被忽略或者整条解析失败——具体怎么处理看实现。

二、服务端挑不到怎么办

Demo 里有一组请求点名要 application/xml,服务端只支持三种格式。 结果是回 200,正文用了默认的 text/plain。 严格说这时候该回 406 Not Acceptable。 但现实里几乎没人这么做——因为回 406 对用户意味着「打不开」,而给一个能看的替代品通常更善意。 这也是内容协商这套机制的一个现实特点:它是个建议,不是合同。 服务端可以做它认为合理的决定,只要在 Vary 里说清依据就行。

三、三个容易忽略的细节

通配符只能用在后半段:text/* 合法,*/html 不合法。*/* 表示什么都行。 更具体的类型优先:text/html 和 text/* 同时出现时,即使 q 值一样,也该挑更具体的那个。 Accept 只管「媒体类型」,不保证服务端一定实现。 你得在响应里看实际的 Content-Type,不能假设它等于请求里排第一的那个。

四、动手 Demo:六组 Accept,看服务端怎么选

实现一个极简的协商:把 Accept 拆成「媒体类型 + q 值」的列表,按 q 从大到小排, 挑第一个服务端支持的。

def responder(raw, st):
    first, hd, _b = H.parse_headers(raw)
    cands = parse_q(hd.get(u'accept', u'*/*'))
    st[u'cands'] = cands
    cands.sort(key=lambda x: -x[1])     # q 大的在前
    best = cands[0][0] if cands else u'*/*'
    if best in (u'application/json', u'*/*'):
        body, ct = b'{"ok":true}', u'application/json'
    elif best in (u'text/html', u'application/xhtml+xml'):
        body, ct = b'<html><body>ok</body></html>', u'text/html'
    else:
        body, ct = b'ok', u'text/plain'
    return reply(u'HTTP/1.1 200 OK', [(u'Content-Type', ct)], body)

实跑输出:

--------------------------------------------------------------------
Accept 的 q 值:同一份数据,四种交付格式
--------------------------------------------------------------------
  【只要 HTML】
    Accept                   text/html
    解析出的候选             text/html(q=1)
    服务端选了               text/html
    响应                     HTTP/1.1 200 OK | 140 字节

  【只要 JSON】
    Accept                   application/json
    解析出的候选             application/json(q=1)
    服务端选了               application/json
    响应                     HTTP/1.1 200 OK | 130 字节

  【HTML 优先,JSON 次之】
    Accept                   text/html, application/json;q=0.5
    解析出的候选             text/html(q=1) | application/json(q=0.5)
    服务端选了               text/html
    响应                     HTTP/1.1 200 OK | 140 字节

  【JSON 优先,HTML 次之】
    Accept                   application/json, text/html;q=0.5
    解析出的候选             application/json(q=1) | text/html(q=0.5)
    服务端选了               application/json
    响应                     HTTP/1.1 200 OK | 130 字节

  【什么都行】
    Accept                   */*
    解析出的候选             */*(q=1)
    服务端选了               application/json
    响应                     HTTP/1.1 200 OK | 130 字节

  【点名要一个服务端没有的】
    Accept                   application/xml
    解析出的候选             application/xml(q=1)
    服务端选了               text/plain
    响应                     HTTP/1.1 200 OK | 114 字节

  q 值是「偏好权重」,0 到 1 之间,越大越想要。
  `text/html, application/json;q=0.5` 的意思是「HTML 最好,JSON 也能凑合」。
  最后那个 `application/xml` 服务端给不了,实际会退到默认格式回 200——
  严格的做法是回 406 Not Acceptable,但现实里几乎没人这么做,
  因为回 406 对用户来说等于「打不开」,不如给个替代品。

text/html 与 application/json 各自单发时,服务端都照着给,响应大小分别是 140 和 130 字节。 两组带 q 值的对照很能说明问题:HTML 在前选 HTML(140 字节),JSON 在前选 JSON(130 字节)。 顺序加上 q 值,就是完整的偏好表达。 */* 那一组服务端选了 JSON——因为代码里 */* 走进了 JSON 分支。 这只是本 Demo 的默认选择,规范没规定必须选哪个。 最后一组要 application/xml,服务端没有,退到 text/plain,回 200。 114 字节的响应——比另两种都小,因为正文只有两个字节。

30 秒实验

  1. 把最后那组改成服务端有 xml 分支,看会不会真的给出 xml。
  2. 发 text/html;q=0,看服务端会不会刻意避开 HTML。
  3. 想一想:为什么 Vary: Accept 是必需的?不加会出什么事?

常见误区

  1. 「Accept 决定服务端返回什么」——它只是表达客户端偏好,服务端可以不听。
  2. 「q 值是百分比」——是 0 到 1 的权重,不是百分数。
  3. 「请求里排第一的一定会拿到」——还要看服务端支持什么。
自测三问(点开对答案) 1. `text/html, application/json;q=0.5` 是什么意思? —— HTML 优先(q=1),JSON 次之(q=0.5),两者都能接受。 2. 服务端没有任何客户端要的格式时,严格说该回什么?现实中多回什么? —— 严格说回 406 Not Acceptable;现实中多回 200 加一个默认格式。 3. 通配符 `*/html` 合法吗? —— 不合法。通配符只能出现在后半段,比如 `text/*`。

格式协商说完了。接下来三个都是同一族的:字符集、编码、语言。

相关笔记