HTTP状态码:从请求的一生重新理解
"所有数字背后,都是一个请求的遗言。"
------ 某位被502逼疯的工程师
或者,你也可以记住这一句:
"状态码不是用来背的,是用来收尸的。"
------ 同一位工程师,在又一次凌晨三点被叫起来之后
为什么写这篇文章
相信很多朋友面试的时候都会被面试官问到:"你记得多少HTTP状态码?具体有哪些含义?"
一般对于这类问题,我们都会提前复习和记忆,才能回答得比较完整。
但后来我发现一件事:
"背状态码就像背尸检报告,你记住了死因,却没见过现场。"
------ 某位靠背答案转行写代码的面试者
本文从一个请求离开客户端之后的链路出发,带你去看现场。
要彻底搞懂HTTP状态码,我们可以换一种思路:设计这么多状态码,它们具体是在哪一环节、因为什么原因被返回的?
我们的请求传递到整个后端,并不是直接访问到服务器。它要经过一大批网络组件的筛选过滤,每一关都有可能倒下,每一关都会有人替它写下遗言。
下面,让我们从一个请求发送到后端的链路,重新认识一下HTTP状态码。
第一站:边缘节点 CDN
"我以为我能活到源站,结果在门口就被拦下了。"
------ 一个试图直接访问服务器的请求
当一个请求经过DNS解析离开设备,遇到的第一个网络组件是CDN。
CDN是一种缓存设备。它把源服务器的资源拉取到离你最近的地方,像个热情过度的前台:"你要这个?我这有,别往里跑了。"
遇到热门的资源文件(比如B站、抖音的热门视频),直接从CDN获取,速度远远快于访问远端的服务器。对于这些占用带宽较大的静态文件资源,缓存到CDN上是性价比最高的方案。
CDN节点状态码
| 状态码 | 含义 | 死因报告 |
|---|---|---|
200 |
成功命中 | "我这有,拿去吧。" |
304 |
未修改 | "你手里的还是新鲜的,不用换。" |
502 |
回源非法响应 | "我去帮你问,结果源站说方言,听不懂。" |
503 |
回源连接拒绝 | "源站把门关上了,不让我进。" |
504 |
回源超时 | "源站接了电话,但一直不说话。" |
💡 304是个好东西
每次向CDN发起请求,并不一定需要CDN把整个文件再发一遍。
如果我们本地有缓存,带着文件的指纹(ETag)或修改时间(Last-Modified)去问CDN:"我这个还新鲜吗?"
CDN看一眼:"没变,接着用吧。"
省带宽,省时间,双方都舒服。
------ 这是唯一一个**请求和服务器达成共识"你不用干活"**的状态码。
第二站:安全网关 WAF
"我不是不让你进,我是怕你进来搞破坏。"
------ WAF,一个没有感情的安检机器
请求离开CDN后,仍然不能直接到达源站。它先要经过WAF------Web应用防火墙。
这个组件的作用,名字已经写得很清楚:为了安全。
它像个眼神锐利的保安,把你从头扫到脚:
- 检查IP是否合法 → 不合法返回
403 Forbidden - 检查请求头是否合法 → 不合法返回
406 Not Acceptable - 检查请求体是否合法 → 不合法返回
413 Payload Too Large - 判断请求频率是否正常 → 不合法返回
429 Too Many Requests
WAF状态码场景
| 攻击/异常类型 | 状态码 | 死因报告 |
|---|---|---|
| 黑名单IP/SQL注入/XSS | 403 |
"你身上有刀,不许进。" |
| 无效Accept头 | 406 |
"你要的东西我给不了,别进了。" |
| 超大请求体 | 413 |
"你扛的箱子太大了,进不来。" |
| CC攻击/高频请求 | 429 |
"你来回跑太多次了,歇会儿。" |
这些"不正经"的请求方式,其实就是网络安全课里讲的攻击手段。
"我只是想进来看看,它说我是黑客。"
------ 一个带着正常User-Agent却被误杀的公司内网爬虫
第三站:负载均衡器 Nginx
"一万个用户就要一万个进程?凭什么等网速还要占着位置?"
------ Igor Sysoev,Nginx之父,2002年
对于这个组件,一开始我也不明白它为什么有那么多功能。
要认识一件东西,最好的方式是了解它为什么被创造出来。
2002年,莫斯科。
Apache的规矩:来一个人开一个进程,来一万个人开一万个进程。
16G内存,Apache张嘴要50G,然后跪了。
Igor Sysoev每天的工作就是重启服务器------像给同一个病人反复做心肺复苏。
终于有一天他骂了句脏话:
"一万个用户就要一万个进程?凭什么等网速的时候还要占着内存?"
他觉得这不合理------像每个客人身后站一个专属服务员,客人上厕所他都得站着等。
Igor决定写一个"不讲武德"的服务器:
一个服务员管五十桌,谁招手过去,谁看菜单就晾着。不等人,不空转,不占茅坑。
两年,一万行C。
2004年,Nginx诞生。
4个进程扛1万连接,内存500MB。
Apache用50G干的活,它用1%的资源。
后来有人问他为什么写Nginx。
他说:
"等的时候,不应该占着位置。"
Nginx核心状态码
| 场景 | 状态码 | 死因报告 |
|---|---|---|
| 静态文件不存在 | 404 |
"你要的文件,硬盘里没有。" |
| 静态文件无权限 | 403 |
"文件在那,但你不配看。" |
| 后端无响应 | 502 |
"我把请求转给后面,后面没人接。" |
| 后端超时 | 504 |
"后面接了电话,但一直'嗯'个不停,就是不说话。" |
| 客户端提前关闭 | 499 |
"用户等不及,把网页关了。" |
| 限流拦截 | 429 |
"你刷太快了,我伺候不动。" |
| 主动熔断 | 503 |
"后面的兄弟都快累死了,我先替你挡一下。" |
💡 关于499
499是Nginx独有的状态码。它不是后端返回的,不是WAF拦截的,是Nginx自己记下的遗言:
"他没等我,他走了。"
很多时候你以为的超时(504),其实是用户等得不耐烦,直接关掉了页面。
Nginx默默在日志里写下一行: "请求已转发,但客户端已失联。"
第四站:Web 应用
"终于到我了。"
------ 一个请求,在穿过CDN、WAF、Nginx之后
终于,请求到达了后端应用。
这里的HTTP状态码,是开发者在代码里亲手写下的。
它是唯一一个由你决定生死的环节。
4xx:你的问题,不是我的问题
"你发过来的东西,我尽力了,真的看不懂。"
------ 应用对400说
| 状态码 | 含义 | 死因报告 |
|---|---|---|
400 |
我看不懂 | JSON少括号、类型传错、必填字段没带 |
401 |
你没登录 | 没带Token、Token过期、Token被篡改 |
403 |
你不能进 | 普通用户点管理员接口、IP不在白名单 |
404 |
我没有 | 查不存在的用户ID、已下架的商品 |
409 |
已经有了 | 用户名被占用、重复提交、两人同时编辑同一条数据 |
422 |
内容不对 | 邮箱格式正确但未注册、年龄传了200岁 |
"你说你叫admin,但我这已经有叫admin的了。"
------ 409 Conflict,注册接口的日常
2xx:一切顺利
"今天是个好日子。"
------ 200 OK,最幸福的状态码
| 状态码 | 含义 | 遗言(活着的遗言) |
|---|---|---|
200 |
成功 | "成了,数据给你。" |
201 |
创建成功 | "成了,新资源在这。" |
202 |
已接受 | "收下了,后面慢慢弄。" |
204 |
成功,无返回 | "成了,但没啥可说的。" |
3xx:别找我,去那边
"我已经搬家了,这是新地址。"
------ 301,一个负责任的旧门牌
| 状态码 | 含义 | 死因报告 |
|---|---|---|
301 |
永久搬家 | "这里不住了,以后去那边找我。" |
302 |
临时离开 | "现在不在,你先去隔壁。" |
304 |
没变 | "你手里那个还能用,别下载了。" |
5xx:我炸了,不是你的错
"对不起,是我的问题。"
------ 500 Internal Server Error,一个有礼貌的崩溃
| 状态码 | 含义 | 死因报告 |
|---|---|---|
500 |
代码崩溃 | 空指针、数据库连不上、try-catch没接住 |
502 |
上游乱说话 | 第三方API返回乱码、Redis数据结构不对 |
503 |
我拒绝 | 连接池满了、服务正在重启 |
504 |
上游太慢 | 第三方API超时、SQL查了10秒 |
"我调了别人的接口,别人没回我。"
------ 504,一个被上游坑死的请求
链路简图 · 请求的一生
"这不是架构图,这是事故多发路段示意图。"
写在最后:状态码不是数字,是请求的"尸检报告"
行文至此,我们已经陪着一个HTTP请求走完了它的完整一生。
它从你的浏览器出发,叩开CDN的大门,穿过WAF的安检,经过Nginx的调度,最终抵达应用服务器的后厨。
而在每一道关卡,都有可能倒下------也可能凯旋。
每一个状态码,都不是随机数字,而是请求倒下的那一刻,最后一个活着的人替它写下的死因报告。
当你再看到502,你脑海里应该浮现的不是"Bad Gateway"这行英文,而是一场事故现场:
- 也许是CDN回源时,源站说了句它听不懂的方言(非法响应)
- 也许是Nginx转发时,后端的应用根本没在听(连接失败)
- 也许是你的代码调用第三方API,对方接了电话但开始沉默(超时)
- 也许是负载均衡器巡视一圈,发现所有小弟都已阵亡(无可用后端)
同一个502,七种死法。症状相同,病灶各异。
这就是为什么,学会背状态码的人只能回答"它是什么意思",而理解链路的人能回答:
"它死在了哪一环。"
这趟旅程也告诉我们另一件事:
CDN会替你背锅,Nginx会替你扛压,WAF会替你挡刀------但它们都只是过客。
唯一从头到尾、从生到死都陪着你代码的,是你自己写的业务逻辑。
200是你写的,404是你写的,500也是你写的。
状态码不是面试官拷问你的工具,而是你的代码和这个世界对话的语言。
你用200说:"一切正常。"
你用404说:"你找的东西不在这里。"
你用500说:"抱歉,我出了点问题,已经在看日志了。"
所以,别再背状态码了。
去理解你请求走过的路,去读懂每一行日志,去亲手写下每一个你返回的状态码。
当你不再问"502是什么意思",而是问------
"这个502是谁报的?"
"在哪一环报的?"
"日志里留下了什么线索?"
那一刻,你就不再是背答案的人,而是真的懂了。
"愿你的200永远不鸽,愿你的5xx永远有日志可查。"
------ 同一位被502逼疯的工程师,在最后一次上线后说
