HTTP 协议详解教程 / 第 5 章:状态码详解
第 5 章:状态码详解
状态码是服务器对请求处理结果的数字表达。正确使用状态码能让客户端快速理解响应意图,是设计良好 API 的关键。
5.1 状态码结构
HTTP/1.1 404 Not Found
─┬─ ────┬────
│ │
数字 原因短语
─┬─
│
┌────┴────┬───────────┐
1xx 2xx 3xx
信息性 成功 重定向
4xx 5xx
客户端错误 服务器错误
| 分类 | 范围 | 含义 | 客户端行为 |
|---|
| 1xx | 100-199 | 信息性 | 继续当前操作 |
| 2xx | 200-299 | 成功 | 处理响应数据 |
| 3xx | 300-399 | 重定向 | 跟随重定向或显示新位置 |
| 4xx | 400-499 | 客户端错误 | 修正请求后重试 |
| 5xx | 500-599 | 服务器错误 | 稍后重试 |
5.2 1xx — 信息性状态码
1xx 表示请求已被接收,客户端应继续。
100 Continue
# 客户端发送大请求体前,先询问服务器是否接受
PUT /api/upload HTTP/1.1
Host: example.com
Content-Length: 104857600
Expect: 100-continue
# 服务器确认
HTTP/1.1 100 Continue
# 客户端继续发送请求体
<binary data...>
import requests
# requests 库自动处理 100 Continue
with open('large-file.zip', 'rb') as f:
response = requests.put(
'https://api.example.com/upload',
data=f,
headers={'Content-Type': 'application/octet-stream'}
)
101 Switching Protocols
# WebSocket 握手时使用
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
102 Processing (WebDAV)
# 服务器正在处理长时间运行的请求
# 客户端应等待最终响应
HTTP/1.1 102 Processing
103 Early Hints
# 在最终响应前发送预加载提示
HTTP/1.1 103 Early Hints
Link: </style.css>; rel=preload; as=style
Link: </script.js>; rel=preload; as=script
HTTP/1.1 200 OK
Content-Type: text/html
...
# Nginx 配置 Early Hints
location / {
add_header Link "</style.css>; rel=preload; as=style";
add_header Link "</script.js>; rel=preload; as=script";
proxy_pass http://backend;
}
5.3 2xx — 成功状态码
2xx 表示请求已被成功处理。
200 OK
GET /api/users/123 HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 85
{"id":123,"name":"Alice","email":"alice@example.com"}
201 Created
POST /api/users HTTP/1.1
Content-Type: application/json
{"name":"Bob","email":"bob@example.com"}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/456
{"id":456,"name":"Bob","email":"bob@example.com"}
202 Accepted
# 请求已接受,但处理尚未完成
POST /api/reports/generate HTTP/1.1
Content-Type: application/json
{"type":"monthly","month":"2026-04"}
HTTP/1.1 202 Accepted
Content-Type: application/json
{"task_id":"abc-123","status":"processing","check_url":"/api/tasks/abc-123"}
204 No Content
DELETE /api/users/123 HTTP/1.1
HTTP/1.1 204 No Content
# 204 没有响应体
response = requests.delete('https://api.example.com/users/123')
if response.status_code == 204:
print("删除成功,无响应体")
# response.text 为空
206 Partial Content
# 断点续传或分块下载
GET /files/large.zip HTTP/1.1
Range: bytes=1024-2047
HTTP/1.1 206 Partial Content
Content-Range: bytes 1024-2047/10240
Content-Length: 1024
<binary data...>
import requests
# 断点续传 — 下载大文件
def download_file(url, filename):
# 获取文件大小
head = requests.head(url)
total_size = int(head.headers['Content-Length'])
downloaded = 0
with open(filename, 'wb') as f:
while downloaded < total_size:
end = min(downloaded + 1024 * 1024, total_size - 1)
headers = {'Range': f'bytes={downloaded}-{end}'}
response = requests.get(url, headers=headers)
f.write(response.content)
downloaded = end + 1
print(f"下载进度: {downloaded}/{total_size}")
5.4 3xx — 重定向状态码
301 Moved Permanently
GET /old-page HTTP/1.1
HTTP/1.1 301 Moved Permanently
Location: /new-page
# Nginx 永久重定向
location /old-page {
return 301 /new-page;
}
# HTTP 到 HTTPS 重定向
server {
listen 80;
return 301 https://$host$request_uri;
}
302 Found
# 临时重定向(浏览器可能改 POST 为 GET)
GET /temp-redirect HTTP/1.1
HTTP/1.1 302 Found
Location: /other-page
303 See Other
# POST 后重定向到结果页面
POST /api/orders HTTP/1.1
Content-Type: application/json
{"product_id": 42}
HTTP/1.1 303 See Other
Location: /orders/789
304 Not Modified
GET /api/users HTTP/1.1
If-None-Match: "etag-123"
HTTP/1.1 304 Not Modified
ETag: "etag-123"
Cache-Control: max-age=3600
307 Temporary Redirect
# 临时重定向(保留请求方法)
POST /api/action HTTP/1.1
Content-Type: application/json
{"data":"value"}
HTTP/1.1 307 Temporary Redirect
Location: /api/new-action
# 客户端应使用 POST 重定向
308 Permanent Redirect
# 永久重定向(保留请求方法)
POST /api/old-endpoint HTTP/1.1
HTTP/1.1 308 Permanent Redirect
Location: /api/new-endpoint
重定向方法保留对比
| 状态码 | 永久/临时 | 方法保留 |
|---|
| 301 | 永久 | 不保留(浏览器改 POST 为 GET) |
| 302 | 临时 | 不保留 |
| 303 | 临时 | 改为 GET |
| 307 | 临时 | 保留 |
| 308 | 永久 | 保留 |
5.5 4xx — 客户端错误
常用 4xx 状态码
| 状态码 | 名称 | 使用场景 |
|---|
| 400 | Bad Request | 请求格式错误、参数验证失败 |
| 401 | Unauthorized | 未提供或无效的认证凭据 |
| 403 | Forbidden | 已认证但无权限访问 |
| 404 | Not Found | 资源不存在 |
| 405 | Method Not Allowed | 请求方法不被允许 |
| 408 | Request Timeout | 请求超时 |
| 409 | Conflict | 资源冲突(如重复创建) |
| 410 | Gone | 资源已永久删除 |
| 413 | Payload Too Large | 请求体过大 |
| 415 | Unsupported Media Type | 不支持的媒体类型 |
| 422 | Unprocessable Entity | 语义错误(格式正确但业务逻辑错误) |
| 429 | Too Many Requests | 超过速率限制 |
400 Bad Request
POST /api/users HTTP/1.1
Content-Type: application/json
{"email": "invalid-email"}
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数验证失败",
"details": [
{"field": "email", "message": "邮箱格式不正确"}
]
}
}
401 vs 403
# 401 Unauthorized — 未认证
GET /api/admin/users HTTP/1.1
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api"
Content-Type: application/json
{"error":{"code":"UNAUTHORIZED","message":"请先登录"}}
# 403 Forbidden — 已认证但无权限
GET /api/admin/users HTTP/1.1
Authorization: Bearer <valid-token-without-admin-role>
HTTP/1.1 403 Forbidden
Content-Type: application/json
{"error":{"code":"FORBIDDEN","message":"无管理员权限"}}
404 Not Found
GET /api/users/99999 HTTP/1.1
HTTP/1.1 404 Not Found
Content-Type: application/json
{"error":{"code":"NOT_FOUND","message":"用户不存在"}}
409 Conflict
POST /api/users HTTP/1.1
Content-Type: application/json
{"email": "alice@example.com"}
HTTP/1.1 409 Conflict
Content-Type: application/json
{"error":{"code":"CONFLICT","message":"该邮箱已被注册","existing_user_id":123}}
429 Too Many Requests
GET /api/data HTTP/1.1
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1715328000
Content-Type: application/json
{"error":{"code":"RATE_LIMITED","message":"请求过于频繁,请 60 秒后重试"}}
import requests
import time
def request_with_retry(url, max_retries=3):
for attempt in range(max_retries):
response = requests.get(url)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
print(f"速率限制,等待 {retry_after} 秒...")
time.sleep(retry_after)
continue
return response
raise Exception("超过最大重试次数")
5.6 5xx — 服务器错误
常用 5xx 状态码
| 状态码 | 名称 | 使用场景 |
|---|
| 500 | Internal Server Error | 服务器内部错误 |
| 501 | Not Implemented | 方法未实现 |
| 502 | Bad Gateway | 网关/代理从上游收到无效响应 |
| 503 | Service Unavailable | 服务暂时不可用(过载/维护) |
| 504 | Gateway Timeout | 网关超时 |
| 507 | Insufficient Storage | 存储空间不足 (WebDAV) |
500 Internal Server Error
GET /api/users HTTP/1.1
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
{
"error": {
"code": "INTERNAL_ERROR",
"message": "服务器内部错误",
"request_id": "req-abc-123"
}
}
503 Service Unavailable
HTTP/1.1 503 Service Unavailable
Retry-After: 120
Content-Type: text/html
<html>
<body>
<h1>503 Service Unavailable</h1>
<p>系统维护中,预计 2 分钟后恢复。</p>
</body>
</html>
# Nginx 维护页面
server {
listen 80;
server_name example.com;
# 维护模式
if (-f /tmp/maintenance.flag) {
return 503;
}
error_page 503 @maintenance;
location @maintenance {
rewrite ^(.*)$ /maintenance.html break;
}
}
5.7 自定义状态码
虽然 HTTP 定义了标准状态码,但你可以在 API 中使用自定义状态码。
使用规范
| 规则 | 说明 |
|---|
| 使用 4xx/5xx 范围 | 不要使用 1xx、2xx、3xx 自定义 |
| 在响应体中说明 | 自定义状态码必须在响应体中提供详细说明 |
| 记录在文档中 | 客户端需要知道如何处理 |
# 自定义状态码示例
from http.server import HTTPServer, BaseHTTPRequestHandler
class CustomHandler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == '/api/payment':
# 自定义 451 表示需要支付
self.send_response(451)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(b'{"error":{"code":"PAYMENT_REQUIRED","message":"请先支付"}}')
elif self.path == '/api/blocked':
# 自定义 450 表示被封禁
self.send_response(450)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(b'{"error":{"code":"BLOCKED","message":"账号已被封禁"}}')
451 Unavailable For Legal Reasons
# RFC 7725 — 因法律原因不可用
GET /content/forbidden-article HTTP/1.1
HTTP/1.1 451 Unavailable For Legal Reasons
Link: <https://legal-info.example.com>; rel="blocked-by"
Content-Type: application/json
{"error":{"code":"LEGAL_BLOCK","message":"该内容因法律原因不可用"}}
5.8 业务场景:完整错误响应结构
标准错误响应格式
{
"error": {
"code": "VALIDATION_ERROR",
"message": "请求参数验证失败",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "邮箱格式不正确"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "年龄必须在 0-150 之间"
}
],
"request_id": "req-550e8400-e29b-41d4",
"documentation_url": "https://api.example.com/docs/errors#VALIDATION_ERROR"
}
}
Express.js 统一错误处理
// 错误中间件
class AppError extends Error {
constructor(statusCode, code, message, details = null) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
}
}
// 使用
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await db.users.findById(req.params.id);
if (!user) {
throw new AppError(404, 'NOT_FOUND', '用户不存在');
}
res.json(user);
} catch (err) {
next(err);
}
});
// 全局错误处理
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const response = {
error: {
code: err.code || 'INTERNAL_ERROR',
message: statusCode === 500 ? '服务器内部错误' : err.message,
request_id: req.headers['x-request-id']
}
};
if (err.details) {
response.error.details = err.details;
}
// 生产环境隐藏 500 错误详情
if (statusCode === 500 && process.env.NODE_ENV === 'production') {
console.error(err);
}
res.status(statusCode).json(response);
});
5.9 状态码选择最佳实践
RESTful API 状态码速查表
| 场景 | 状态码 | 说明 |
|---|
| 获取资源成功 | 200 | 正常返回 |
| 创建资源成功 | 201 | 附带 Location 头 |
| 删除资源成功 | 204 | 无响应体 |
| 异步任务已接受 | 202 | 返回任务 ID |
| 永久重定向 | 301 / 308 | 308 保留方法 |
| 临时重定向 | 302 / 307 | 307 保留方法 |
| 缓存有效 | 304 | 无响应体 |
| 请求格式错误 | 400 | 参数验证失败 |
| 未认证 | 401 | 需要登录 |
| 无权限 | 403 | 权限不足 |
| 资源不存在 | 404 | 路由或资源 |
| 方法不允许 | 405 | 附带 Allow 头 |
| 冲突 | 409 | 重复创建等 |
| 业务逻辑错误 | 422 | 格式正确但语义错误 |
| 限流 | 429 | 附带 Retry-After |
| 服务器错误 | 500 | 通用服务器错误 |
| 服务不可用 | 503 | 维护或过载 |
⚠️ 注意事项
- 401 vs 403:401 是"你是谁"(认证),403 是"你能干什么"(授权)
- 404 vs 410:404 是暂时不存在,410 是永久删除
- 400 vs 422:400 是格式错误,422 是语义错误(格式正确但业务逻辑不对)
- 500 错误隐藏:生产环境不要将堆栈跟踪暴露给客户端
- 503 附带 Retry-After:告知客户端何时重试
- 429 附带限流信息:X-RateLimit-* 头部帮助客户端自我调节
🔗 扩展阅读
下一章:第 6 章:HTTP 头部字段 — 请求头/响应头、Content-Type、Cache-Control、自定义头