📘 第四章 返回结果的HTTP状态码
📄 一句话概括

400 是「请求本身不合法」的总兜底:语法错、缺必备首部、字段格式不对。原样重试一万次也没用。

📝 本节和前面几节的关系

3XX 说的是「去别处」,4XX 说的是「你错了」。 这一节是 4XX 里最常见的那个,也是排查时最该先看的。

一、什么情况会回 400

原因 例子 谁的问题
缺必备首部 HTTP/1.1 没带 Host 客户端/代理
请求行不合语法 方法名里有空格 客户端
首部行不合语法 没有冒号 客户端
长度对不上 Content-Length 与实际不符 客户端
URL 有非法字符 路径里有裸空格 客户端
共同点:还没走到业务逻辑就被拒了。所以 400 的错误日志里通常看不到任何业务信息,只有原始请求。
Demo 里用四条畸形请求实测,每条服务端都能说出具体的拒绝原因——这也是好实践:
400 的正文里写清哪里错了,别只说 bad request。

二、400 与 422 的分工

400 是「语法层面就不对」; 422 Unprocessable Entity 是「语法没问题,但语义不合法」(比如年龄填了 -5、邮箱格式不对)。 422 是 WebDAV 引入的,现在被 REST API 广泛借用。 很多框架默认把「校验失败」回 400,也不算错,但 422 更精确。 再往下还有 409 Conflict(状态冲突)、412 Precondition Failed(前置条件不满足)、 428 Precondition Required。这几个都属于「请求能被理解但没法执行」,只是原因不同。

三、一个容易被忽略的现实

线上偶尔会有一批来源集中的 400,比如某个运营商网关、某个老版本 App、某个爬虫。 那不是你的 bug,但你要去查——因为它在吃你的带宽和日志配额。 做法通常是:按 UA 或源 IP 聚合,看是不是同一个客户端在刷; 如果是自家老版本 App,就得考虑在网关层兜住,而不是让后端一直报错。 还有一条:网关回 400 和后端回 400 长得一样,但一个是 Nginx 拦的、一个是框架抛的。 看日志要先确认是谁回的——响应里有没有你的应用特有的首部,就是最快的判据。

四、动手 Demo:四条畸形请求,四种拒绝原因

服务端按 RFC 7230 的几条硬规矩校验:请求行必须三段、版本必须是 HTTP/x.y、首部行必须有冒号、 HTTP/1.1 必须有 Host。

def validate(raw):
    lines = raw.split(b'\r\n\r\n')[0].decode('latin-1').split('\r\n')
    parts = lines[0].split(' ')
    if len(parts) != 3:
        return u'请求行必须恰好三段,实际 %d 段' % len(parts)
    for ln in lines[1:]:
        if ln and ':' not in ln:
            return u'首部行没有冒号:%s' % ln
    if ver == 'HTTP/1.1' and 'host' not in hd:
        return u'HTTP/1.1 缺 Host'

实跑输出:

--------------------------------------------------------------------
400 Bad Request:请求本身就不合法
--------------------------------------------------------------------
  缺 Host(HTTP/1.1 必备)       HTTP/1.1 400 Bad Request
      拒绝原因                   HTTP/1.1 缺 Host
  请求行缺版本                   HTTP/1.1 400 Bad Request
      拒绝原因                   请求行必须恰好三段,实际 2 段
  方法名带空格                   HTTP/1.1 400 Bad Request
      拒绝原因                   请求行必须恰好三段,实际 4 段
  首部行没有冒号                 HTTP/1.1 400 Bad Request
      拒绝原因                   首部行没有冒号:Host 127.0.0.1

  400 的含义                 语法错、框架错、字段缺——总之是请求本身有问题
  400 该谁修                 客户端。原样重试一万次也没用
  最常见的真凶               缺 Host、Content-Length 与实际不符、URL 里有非法字符
  线上偶尔会看到「某运营商网关发来的畸形请求」,
  那种 400 在服务端日志里会有一大批同源 IP——不是你的 bug,但要去查。

四行全是 400 Bad Request,但拒绝原因各不相同:缺 Host、请求行只有 2 段、请求行有 4 段、 首部行没有冒号。 「方法名带空格」那条最容易被忽略:GE T / HTTP/1.1 看起来像「方法写错了」, 实际是请求行被切成了四段。 不同的校验器会给出完全不同的报错,这也是排查 400 时常觉得莫名其妙的原因。 这四条规矩都是 RFC 7230 明文写的。真实服务器会校验更多,比如首部名不能有空格、值不能用裸控制字符。

30 秒实验

  1. 把 Content-Length: 10 但只发 5 字节,看服务端会不会超时后回 400。
  2. 在路径里塞一个裸空格(GET /a b HTTP/1.1),看会落到哪条校验上。
  3. 用 curl 发一个不带 Host 的请求(curl -H 'Host:'),看真实服务器回什么。

常见误区

  1. 「400 是服务端 bug」——它是客户端错,方向别搞反。
  2. 「400 说明业务逻辑有错」——它连业务逻辑都没走到,在解析阶段就被拒了。
  3. 「所有参数错误都该回 400」——语义错误更该用 422,冲突用 409。
自测三问(点开对答案) 1. 400 通常在哪一步被拒? —— 请求解析阶段,还没进业务逻辑。 2. HTTP/1.1 缺 Host 会怎样? —— 服务端应当回 400,因为 Host 是 HTTP/1.1 的必备首部。 3. 参数格式对但语义不合法该用哪个码? —— 422 Unprocessable Entity。

400 是「你的请求写坏了」。下一节看「你没说你是谁」。

相关笔记