HTTP 协议详解教程 / 第 4 章:请求方法详解
第 4 章:请求方法详解
请求方法是 HTTP 语义的核心,正确选择方法不仅让 API 更清晰,还能利用缓存、幂等性等协议特性。
4.1 方法的三个维度
在选择方法前,理解三个关键概念:
| 概念 | 定义 | 影响 |
|---|---|---|
| 安全(Safe) | 不会修改服务器资源 | 可被缓存、预取 |
| 幂等(Idempotent) | 多次执行效果相同 | 重试安全 |
| 缓存(Cacheable) | 响应可被缓存 | GET 默认可缓存 |
| 方法 | 安全 | 幂等 | 可缓存 | 有请求体 |
|---|---|---|---|---|
| GET | ✓ | ✓ | ✓ | ✗ |
| HEAD | ✓ | ✓ | ✓ | ✗ |
| OPTIONS | ✓ | ✓ | ✗ | ✗ |
| PUT | ✗ | ✓ | ✗ | ✓ |
| DELETE | ✗ | ✓ | ✗ | ✗ |
| POST | ✗ | ✗ | 条件性 | ✓ |
| PATCH | ✗ | ✗ | ✗ | ✓ |
4.2 GET — 获取资源
GET 是最常用的方法,用于获取资源而不产生副作用。
语义
GET /api/users/123 HTTP/1.1
Host: api.example.com
Accept: application/json
特性
- 必须是安全的:不应修改服务器数据
- 必须是幂等的:多次请求结果相同
- 可缓存:浏览器和代理可缓存响应
- 参数在 URL 中:不适合传输敏感数据
- 无请求体:规范不禁止,但语义上不应有
代码示例
# 基本 GET 请求
curl https://jsonplaceholder.typicode.com/posts/1
# 带查询参数
curl "https://jsonplaceholder.typicode.com/posts?userId=1&limit=5"
# 带自定义头部
curl -H "Accept: application/json" \
-H "Accept-Language: zh-CN" \
-H "If-None-Match: \"etag-value\"" \
https://api.example.com/users
import requests
# 基本请求
response = requests.get('https://jsonplaceholder.typicode.com/posts/1')
print(response.json())
# 带参数
response = requests.get(
'https://jsonplaceholder.typicode.com/posts',
params={'userId': 1, '_limit': 5}
)
print(response.json())
⚠️ 常见错误
# ❌ 错误:GET 不应有副作用
GET /api/users/123?delete=true HTTP/1.1
# ❌ 错误:GET 不应携带敏感数据
GET /api/login?password=secret123 HTTP/1.1
# ✅ 正确:使用 POST/DELETE 处理修改操作
DELETE /api/users/123 HTTP/1.1
4.3 POST — 提交数据
POST 用于向服务器提交数据,通常会创建新资源或触发处理。
语义
- 不安全:会修改服务器状态
- 不幂等:多次提交可能创建多个资源
- 可缓存:某些情况下可缓存(如带 Expires 头)
典型场景
| 场景 | 说明 |
|---|---|
| 创建资源 | POST /api/users → 创建新用户 |
| 提交表单 | POST /login → 用户登录 |
| 上传文件 | POST /upload → 文件上传 |
| 触发操作 | POST /api/orders/123/cancel → 取消订单 |
| RPC 调用 | POST /api/actions/send-email → 发送邮件 |
代码示例
# 提交 JSON
curl -X POST \
-H "Content-Type: application/json" \
-d '{"title":"foo","body":"bar","userId":1}' \
https://jsonplaceholder.typicode.com/posts
# 提交表单
curl -X POST \
-d "username=alice&password=secret" \
https://example.com/login
# 上传文件
curl -X POST \
-F "file=@photo.jpg" \
-F "description=My photo" \
https://example.com/upload
import requests
# JSON 提交
response = requests.post(
'https://jsonplaceholder.typicode.com/posts',
json={'title': 'foo', 'body': 'bar', 'userId': 1}
)
print(response.status_code) # 201
print(response.json())
# 表单提交
response = requests.post(
'https://example.com/login',
data={'username': 'alice', 'password': 'secret'}
)
# 文件上传
with open('photo.jpg', 'rb') as f:
response = requests.post(
'https://example.com/upload',
files={'file': ('photo.jpg', f, 'image/jpeg')},
data={'description': 'My photo'}
)
幂等性问题与解决方案
# POST 不幂等 — 多次提交可能创建多个订单
# 解决方案:使用幂等键(Idempotency Key)
import requests
import uuid
idempotency_key = str(uuid.uuid4())
response = requests.post(
'https://api.example.com/orders',
json={'product_id': 42, 'quantity': 1},
headers={
'Idempotency-Key': idempotency_key, # 相同的 Key 只创建一个订单
}
)
4.4 PUT — 替换资源
PUT 用于替换目标资源的全部状态。
语义
- 幂等:多次执行效果相同
- 整体替换:请求体应包含资源的完整表示
PUT vs POST
| 特性 | PUT | POST |
|---|---|---|
| 语义 | 替换已知资源 | 创建/提交 |
| URL | 客户端知道资源 URL | 服务器决定资源 URL |
| 幂等 | 是 | 否 |
| 多次请求 | 结果相同 | 可能创建多个 |
# PUT — 完整替换资源
curl -X PUT \
-H "Content-Type: application/json" \
-d '{"name":"Alice Updated","email":"alice@new.com","age":30}' \
https://api.example.com/users/123
import requests
# 完整替换用户信息
full_user_data = {
"name": "Alice Updated",
"email": "alice@new.com",
"age": 30,
"role": "admin" # 即使不想修改的字段,也要提供完整数据
}
response = requests.put(
'https://api.example.com/users/123',
json=full_user_data
)
用 PUT 创建资源
# 客户端指定资源 URL
PUT /api/users/456 HTTP/1.1
Content-Type: application/json
{"name":"Bob","email":"bob@example.com"}
// Express.js 处理 PUT
app.put('/api/users/:id', (req, res) => {
const { id } = req.params;
const userData = req.body;
// 查找并替换(如果不存在则创建)
const existingUser = db.users.get(id);
if (existingUser) {
db.users.set(id, { id: parseInt(id), ...userData });
res.json({ id: parseInt(id), ...userData });
} else {
db.users.set(id, { id: parseInt(id), ...userData });
res.status(201).json({ id: parseInt(id), ...userData });
}
});
4.5 DELETE — 删除资源
DELETE 请求删除指定资源。
语义
- 幂等:删除一个已删除的资源,效果相同
- 响应体:可选,通常返回被删除的资源或 204 No Content
# 删除资源
curl -X DELETE https://api.example.com/users/123
# 响应示例
# HTTP/1.1 204 No Content
import requests
response = requests.delete('https://api.example.com/users/123')
print(response.status_code) # 204
# 批量删除
for user_id in [1, 2, 3]:
requests.delete(f'https://api.example.com/users/{user_id}')
软删除模式
// 软删除 — 实际不删除记录,标记为已删除
app.delete('/api/users/:id', async (req, res) => {
await db.users.update({
where: { id: req.params.id },
data: { deleted_at: new Date() }
});
res.status(204).send();
});
// GET 请求自动过滤已删除的记录
app.get('/api/users', async (req, res) => {
const users = await db.users.findMany({
where: { deleted_at: null }
});
res.json(users);
});
4.6 PATCH — 部分更新
PATCH 用于对资源进行部分修改。
PATCH vs PUT
| 特性 | PUT | PATCH |
|---|---|---|
| 更新范围 | 整体替换 | 部分修改 |
| 请求体 | 资源完整表示 | 仅变更字段 |
| 幂等 | 是 | 理论上不必,但建议实现 |
# PATCH — 只更新部分字段
curl -X PATCH \
-H "Content-Type: application/json" \
-d '{"age":31}' \
https://api.example.com/users/123
import requests
# 只更新 age 字段,其他字段保持不变
response = requests.patch(
'https://api.example.com/users/123',
json={'age': 31}
)
print(response.json())
# {"id": 123, "name": "Alice", "email": "alice@example.com", "age": 31}
JSON Merge Patch(RFC 7396)
PATCH /api/users/123 HTTP/1.1
Content-Type: application/merge-patch+json
{"name":"New Name","age":null}
// 服务端处理 Merge Patch
app.patch('/api/users/:id', async (req, res) => {
const patch = req.body;
const user = await db.users.get(req.params.id);
// 合并补丁,null 值表示删除该字段
const updated = { ...user };
for (const [key, value] of Object.entries(patch)) {
if (value === null) {
delete updated[key];
} else {
updated[key] = value;
}
}
await db.users.set(req.params.id, updated);
res.json(updated);
});
JSON Patch(RFC 6902)
[
{ "op": "replace", "path": "/name", "value": "New Name" },
{ "op": "remove", "path": "/age" },
{ "op": "add", "path": "/tags", "value": ["vip"] }
]
# 使用 JSON Patch
curl -X PATCH \
-H "Content-Type: application/json-patch+json" \
-d '[{"op":"replace","path":"/name","value":"New Name"}]' \
https://api.example.com/users/123
4.7 OPTIONS — 预检请求
OPTIONS 用于查询服务器支持的通信选项。
典型用途
# 查询服务器支持的方法
curl -X OPTIONS https://api.example.com/users -i
# 响应
# HTTP/1.1 200 OK
# Allow: GET, POST, PUT, DELETE, OPTIONS
# Content-Length: 0
CORS 预检请求
浏览器在跨域请求前会自动发送 OPTIONS 预检请求:
OPTIONS /api/users HTTP/1.1
Host: api.example.com
Origin: https://frontend.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://frontend.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
4.8 HEAD — 获取头部
HEAD 与 GET 相同,但服务器不返回响应体。常用于:
# 检查资源是否存在
curl -I https://example.com/file.zip
# 响应
# HTTP/1.1 200 OK
# Content-Length: 5242880
# Content-Type: application/zip
# Last-Modified: Wed, 10 May 2026 10:00:00 GMT
常见用途
| 用途 | 示例 |
|---|---|
| 检查资源存在 | HEAD /api/users/123 → 200 或 404 |
| 获取文件大小 | Content-Length 头 |
| 检查更新时间 | Last-Modified 头 |
| 预检缓存 | If-Modified-Since |
import requests
# 检查远程文件大小
response = requests.head('https://example.com/large-file.zip')
if response.status_code == 200:
size = int(response.headers.get('Content-Length', 0))
print(f"文件大小: {size / 1024 / 1024:.2f} MB")
# 检查是否已更新
last_modified = response.headers.get('Last-Modified')
print(f"最后修改: {last_modified}")
4.9 TRACE — 诊断方法
TRACE 沿请求链回显收到的请求,用于诊断。
# 通常被服务器禁用
curl -X TRACE https://example.com/path
# HTTP/1.1 405 Method Not Allowed
⚠️ 安全警告:TRACE 方法存在 XST (Cross-Site Tracing) 风险,生产环境应禁用。
4.10 业务场景:RESTful 资源操作
用户管理 API
GET /api/users → 获取用户列表
GET /api/users/123 → 获取单个用户
POST /api/users → 创建用户
PUT /api/users/123 → 完整替换用户
PATCH /api/users/123 → 部分更新用户
DELETE /api/users/123 → 删除用户
订单流程
GET /api/orders → 获取订单列表
GET /api/orders/456 → 获取订单详情
POST /api/orders → 创建新订单
PATCH /api/orders/456 → 更新订单(如修改地址)
POST /api/orders/456/pay → 支付订单(触发操作)
POST /api/orders/456/cancel → 取消订单
DELETE /api/orders/456 → 删除订单(通常不允许)
完整 Express.js 示例
const express = require('express');
const app = express();
app.use(express.json());
// 用户 CRUD
const users = new Map();
let nextId = 1;
app.get('/api/users', (req, res) => {
const { page = 1, limit = 20 } = req.query;
const allUsers = [...users.values()];
const start = (page - 1) * limit;
res.json({
data: allUsers.slice(start, start + Number(limit)),
total: allUsers.length,
page: Number(page),
limit: Number(limit)
});
});
app.get('/api/users/:id', (req, res) => {
const user = users.get(Number(req.params.id));
if (!user) return res.status(404).json({ error: '用户不存在' });
res.json(user);
});
app.post('/api/users', (req, res) => {
const id = nextId++;
const user = { id, ...req.body, created_at: new Date().toISOString() };
users.set(id, user);
res.status(201).location(`/api/users/${id}`).json(user);
});
app.put('/api/users/:id', (req, res) => {
const id = Number(req.params.id);
const user = { id, ...req.body, updated_at: new Date().toISOString() };
users.set(id, user);
res.json(user);
});
app.patch('/api/users/:id', (req, res) => {
const id = Number(req.params.id);
const existing = users.get(id);
if (!existing) return res.status(404).json({ error: '用户不存在' });
const updated = { ...existing, ...req.body, updated_at: new Date().toISOString() };
users.set(id, updated);
res.json(updated);
});
app.delete('/api/users/:id', (req, res) => {
const id = Number(req.params.id);
if (!users.has(id)) return res.status(404).json({ error: '用户不存在' });
users.delete(id);
res.status(204).send();
});
app.listen(3000);
⚠️ 注意事项
- GET 不应有副作用:违反会导致爬虫、预取、缓存引发意外操作
- POST 不幂等:关键操作需要幂等键(Idempotency Key)
- PUT 替换 vs PATCH 更新:替换用 PUT,修改用 PATCH
- DELETE 幂等:删除不存在的资源应返回 404 或 204,不是 500
- 避免隧道化:不要用 POST 替代所有方法(如
_method=DELETE) - OPTIONS 预检缓存:通过
Access-Control-Max-Age减少预检请求
🔗 扩展阅读
下一章:第 5 章:状态码详解 — 1xx-5xx 分类、自定义状态码、最佳实践