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

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

特性PUTPOST
语义替换已知资源创建/提交
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

特性PUTPATCH
更新范围整体替换部分修改
请求体资源完整表示仅变更字段
幂等理论上不必,但建议实现
# 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);

⚠️ 注意事项

  1. GET 不应有副作用:违反会导致爬虫、预取、缓存引发意外操作
  2. POST 不幂等:关键操作需要幂等键(Idempotency Key)
  3. PUT 替换 vs PATCH 更新:替换用 PUT,修改用 PATCH
  4. DELETE 幂等:删除不存在的资源应返回 404 或 204,不是 500
  5. 避免隧道化:不要用 POST 替代所有方法(如 _method=DELETE
  6. OPTIONS 预检缓存:通过 Access-Control-Max-Age 减少预检请求

🔗 扩展阅读


下一章第 5 章:状态码详解 — 1xx-5xx 分类、自定义状态码、最佳实践