强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

HTTP 协议详解教程 / 第 5 章:状态码详解

第 5 章:状态码详解

状态码是服务器对请求处理结果的数字表达。正确使用状态码能让客户端快速理解响应意图,是设计良好 API 的关键。


5.1 状态码结构

HTTP/1.1 404 Not Found
         ─┬─ ────┬────
          │      │
        数字   原因短语
        ─┬─
         │
    ┌────┴────┬───────────┐
    1xx       2xx         3xx
  信息性    成功         重定向
    4xx       5xx
  客户端错误  服务器错误
分类范围含义客户端行为
1xx100-199信息性继续当前操作
2xx200-299成功处理响应数据
3xx300-399重定向跟随重定向或显示新位置
4xx400-499客户端错误修正请求后重试
5xx500-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":"[email protected]"}

201 Created

POST /api/users HTTP/1.1
Content-Type: application/json

{"name":"Bob","email":"[email protected]"}

HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/users/456

{"id":456,"name":"Bob","email":"[email protected]"}

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 状态码

状态码名称使用场景
400Bad Request请求格式错误、参数验证失败
401Unauthorized未提供或无效的认证凭据
403Forbidden已认证但无权限访问
404Not Found资源不存在
405Method Not Allowed请求方法不被允许
408Request Timeout请求超时
409Conflict资源冲突(如重复创建)
410Gone资源已永久删除
413Payload Too Large请求体过大
415Unsupported Media Type不支持的媒体类型
422Unprocessable Entity语义错误(格式正确但业务逻辑错误)
429Too 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": "[email protected]"}

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 状态码

状态码名称使用场景
500Internal Server Error服务器内部错误
501Not Implemented方法未实现
502Bad Gateway网关/代理从上游收到无效响应
503Service Unavailable服务暂时不可用(过载/维护)
504Gateway Timeout网关超时
507Insufficient 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":"账号已被封禁"}}')
# 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 / 308308 保留方法
临时重定向302 / 307307 保留方法
缓存有效304无响应体
请求格式错误400参数验证失败
未认证401需要登录
无权限403权限不足
资源不存在404路由或资源
方法不允许405附带 Allow 头
冲突409重复创建等
业务逻辑错误422格式正确但语义错误
限流429附带 Retry-After
服务器错误500通用服务器错误
服务不可用503维护或过载

⚠️ 注意事项

  1. 401 vs 403:401 是"你是谁"(认证),403 是"你能干什么"(授权)
  2. 404 vs 410:404 是暂时不存在,410 是永久删除
  3. 400 vs 422:400 是格式错误,422 是语义错误(格式正确但业务逻辑不对)
  4. 500 错误隐藏:生产环境不要将堆栈跟踪暴露给客户端
  5. 503 附带 Retry-After:告知客户端何时重试
  6. 429 附带限流信息:X-RateLimit-* 头部帮助客户端自我调节

🔗 扩展阅读


下一章第 6 章:HTTP 头部字段 — 请求头/响应头、Content-Type、Cache-Control、自定义头