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

500 是「服务端处理请求时出了意外」。责任在服务端,客户端原样重试通常没用。

📝 本节和前面几节的关系

上一节是客户端的错,这一节是服务端的错。 两类错的处理方向完全相反,这也是「第一位定责任方」那条规矩的价值。

一、500 的边界

500 是个兜底码:任何没被更具体的码覆盖的服务端异常,都可以回 500。 空指针、数据库连接断了、配置写错、序列化失败——统统 500。 正因为它是兜底,看到 500 等于「什么都不知道」。 所以好实践是尽量用更具体的码:502 Bad Gateway(上游挂了)、504 Gateway Timeout(上游超时)、 507 Insufficient Storage。 这也解释了为什么监控里 500 的比例是最粗的指标——它只告诉你「有异常」,不告诉你「哪儿异常」。

二、响应里绝不能放什么

堆栈、SQL 语句、文件路径、框架版本、内网 IP、环境变量。 这些在开发环境里很方便,在生产环境就是免费的情报。 攻击者从堆栈里能看出:用的什么框架和版本(对应已知漏洞)、代码部署在哪(对应路径遍历)、 SQL 怎么拼的(对应注入)、连的哪个内网库(对应横向移动)。 正确做法:响应给一句通用提示加一个 trace id,详情只写日志,并且让这个 id 能对上日志。 Demo 里服务端故意触发一个 1/0,回的是 internal error 而不是堆栈。

三、排查 500 的顺序

① 先看日志,别看代码。500 一定有异常栈,堆栈第一行往往就指明了位置。 ② 看是不是新出现的。突然涨说明有变更(发版、配置、依赖升级);一直有说明是常态化的坑。 ③ 看是不是集中在某个接口或某个参数。这能立刻区分「全局故障」和「特定输入」。 ④ 看上游。如果 500 是网关回的,实际可能是后端崩了,这时网关日志和应用日志都要看。

四、动手 Demo:故意崩一次,看响应里留下了什么

服务端对 /boom 触发一个 ZeroDivisionError,捕获后回 500;对 /ok 正常回 200。 观察响应正文里有没有泄漏细节。

if path == u'/boom':
    try:
        1 / 0
    except ZeroDivisionError:
        return (u'HTTP/1.1 500 Internal Server Error',
                [(u'Content-Type', u'text/plain')],
                b'internal error')

实跑输出:

--------------------------------------------------------------------
500 Internal Server Error:服务端自己崩了
--------------------------------------------------------------------
  /ok          HTTP/1.1 200 OK,正文「ok」
  /boom        HTTP/1.1 500 Internal Server Error,正文「internal error」

  500 的含义                 服务器处理请求时出了意外,不是请求的错
  该谁修                     服务端。客户端重试通常也没用(除非是偶发)
  响应里能放什么             **绝不能放堆栈、SQL、文件路径**
  排查靠什么                 一个能对上日志的 trace id,而不是把这些写进响应
  把堆栈回给客户端是常见的新手错误,它的本质是信息泄露——
  攻击者从堆栈里能看出框架版本、目录结构、SQL 结构,直接拿去构造注入。
  500 的正确姿势:响应给一句通用提示 + 一个 id,详情只写日志。

/ok 200 OK,正文「ok」、 /boom 500 Internal Server Error,正文「internal error」——正文里只有一句通用提示, 没有任何堆栈信息。 这就是「该返回什么」的示范。 真实框架(Django、Spring、Express)在生产模式下也是这样, 只有 debug 模式才吐堆栈——上线忘了关 debug 是经典事故。 排查靠什么 一个能对上日志的 trace id——这句是本节最实用的一条。 没有 trace id,用户报「我这儿报错了」你连是哪个请求都找不到。

30 秒实验

  1. 把捕获去掉,让异常直接抛出,看 http.server 自己会回什么(会回 500 加一段 HTML, 里面带异常摘要——这就是不该上线的样子)。
  2. 给响应加一个 X-Trace-Id 首部,想想它和日志怎么对应。
  3. 想一想:500 和 502 的界限在哪?(502 是「上游给的响应不合法」)

常见误区

  1. 「500 是客户端的问题」——正好相反,5XX 是服务端的锅。
  2. 「500 带堆栈方便排查」——方便的是攻击者。
  3. 「所有异常都该回 500」——能用更具体的码就用更具体的,500 是兜底不是首选。
自测三问(点开对答案) 1. 500 的定位是什么? —— 服务端未捕获异常的总兜底码,看到它基本等于「什么都不知道」。 2. 为什么不能在响应里放堆栈? —— 会泄露框架版本、目录结构、SQL 写法等情报,直接可用于构造攻击。 3. 排查 500 的第一步是什么? —— 看日志里的异常栈,而不是先读代码。

500 是「我崩了」。最后一节看「我暂时腾不出手」。

相关笔记