HTTP 请求方法全面解析
HTTP请求方法概述
HTTP协议定义了多种请求方法,用于表示对资源的不同操作类型。这些方法构成了RESTful架构的基础,决定了客户端与服务器交互的方式。在Koa2框架中,理解这些方法的差异和适用场景对构建高效Web应用至关重要。
GET方法
GET是最常用的HTTP方法,用于请求指定资源。它只用于数据获取,不应产生副作用。GET请求的参数会附加在URL后面,通过查询字符串传递。
router.get('/api/users', async (ctx) => {
const users = await User.find();
ctx.body = users;
});
GET请求的特点:
- 可被缓存
- 保留在浏览器历史记录中
- 可被收藏为书签
- 有长度限制(约2048字符)
- 不应处理敏感数据
POST方法
POST用于向指定资源提交数据,通常会导致服务器状态变化。POST请求将数据放在请求体中发送,适合传输大量或敏感数据。
router.post('/api/users', async (ctx) => {
const newUser = new User(ctx.request.body);
await newUser.save();
ctx.status = 201;
ctx.body = newUser;
});
POST请求的典型场景:
- 提交表单数据
- 上传文件
- 创建新资源
- 触发业务流程
PUT方法
PUT用于完整更新资源,客户端需要提供更新后的完整资源。如果资源不存在,PUT可能会创建新资源。
router.put('/api/users/:id', async (ctx) => {
const updatedUser = await User.findByIdAndUpdate(
ctx.params.id,
ctx.request.body,
{ new: true }
);
ctx.body = updatedUser;
});
PUT与POST的关键区别:
- PUT是幂等的(多次调用效果相同)
- PUT需要指定完整资源标识符
- PUT通常用于替换现有资源
PATCH方法
PATCH用于对资源进行部分更新,只需发送需要修改的字段。与PUT不同,PATCH不需要发送完整资源。
router.patch('/api/users/:id', async (ctx) => {
const updatedUser = await User.findByIdAndUpdate(
ctx.params.id,
{ $set: ctx.request.body },
{ new: true }
);
ctx.body = updatedUser;
});
PATCH的常见使用模式:
- JSON Patch格式
- JSON Merge Patch格式
- 自定义部分更新逻辑
DELETE方法
DELETE用于删除指定的资源。成功执行后,服务器应返回204状态码(无内容)或200状态码(包含删除确认信息)。
router.delete('/api/users/:id', async (ctx) => {
await User.findByIdAndDelete(ctx.params.id);
ctx.status = 204;
});
DELETE请求的注意事项:
- 可以是逻辑删除(标记为删除状态)
- 也可以是物理删除(从数据库移除)
- 应考虑级联删除相关资源
HEAD方法
HEAD与GET类似,但服务器只返回头部信息,不返回实际内容。常用于检查资源是否存在或验证缓存有效性。
router.head('/api/users/:id', async (ctx) => {
const userExists = await User.exists({ _id: ctx.params.id });
if (!userExists) ctx.throw(404);
ctx.status = 200;
});
HEAD的典型用途:
- 获取资源的元数据
- 检查链接有效性
- 验证缓存是否过期
OPTIONS方法
OPTIONS用于获取目标资源支持的通信选项。在CORS预检请求中,浏览器会自动发送OPTIONS请求。
router.options('/api/users', async (ctx) => {
ctx.set('Allow', 'GET, POST, OPTIONS');
ctx.status = 204;
});
OPTIONS请求的应用场景:
- CORS预检请求
- 发现服务器支持的方法
- API自描述
TRACE方法
TRACE用于诊断目的,服务器会将收到的请求消息原样返回。Koa2默认不实现TRACE方法,因安全考虑通常应禁用。
// 不建议在生产环境启用TRACE
app.use(async (ctx, next) => {
if (ctx.method === 'TRACE') {
ctx.body = ctx.request;
} else {
await next();
}
});
TRACE的安全风险:
- 可能暴露敏感信息
- 可用于跨站跟踪攻击
- 通常应返回405 Method Not Allowed
CONNECT方法
CONNECT用于建立到目标资源的隧道,通常用于SSL隧道。在Koa2应用中很少需要直接处理CONNECT请求。
// 通常由底层服务器处理CONNECT
app.use(async (ctx, next) => {
if (ctx.method === 'CONNECT') {
ctx.throw(501, 'CONNECT not implemented');
}
await next();
});
CONNECT的主要用途:
- HTTP代理
- SSL隧道
- 建立网络连接
方法特性比较
方法 | 安全 | 幂等 | 缓存 | 请求体 | 响应体 |
---|---|---|---|---|---|
GET | 是 | 是 | 是 | 无 | 有 |
POST | 否 | 否 | 否 | 有 | 有 |
PUT | 否 | 是 | 否 | 有 | 有 |
PATCH | 否 | 否 | 否 | 有 | 有 |
DELETE | 否 | 是 | 否 | 无 | 可有 |
HEAD | 是 | 是 | 是 | 无 | 无 |
OPTIONS | 是 | 是 | 否 | 无 | 有 |
TRACE | 是 | 是 | 否 | 无 | 有 |
CONNECT | 否 | 否 | 否 | 有 | 有 |
Koa2中的方法处理
Koa2的koa-router支持所有标准HTTP方法,并提供简洁的API定义路由:
const Router = require('koa-router');
const router = new Router();
router.get('/resource', handleGet);
router.post('/resource', handlePost);
router.put('/resource/:id', handlePut);
router.del('/resource/:id', handleDelete); // 注意是del不是delete
router.all('/resource', handleAllMethods);
对于不常用的方法,可以使用更灵活的API:
router.register('/resource', ['LOCK', 'UNLOCK'], handleLockMethods);
自定义方法处理
虽然HTTP标准定义了有限的方法,但实际应用中可能需要扩展。Koa2允许处理自定义方法:
app.use(async (ctx, next) => {
if (ctx.method === 'PURGE') {
// 处理缓存清除逻辑
await clearCache();
ctx.status = 204;
} else {
await next();
}
});
常见自定义方法示例:
- PURGE - 清除缓存
- LINK/UNLINK - 资源关联
- VIEW - 只读操作
- COPY/MOVE - 资源操作
方法的安全性与幂等性
理解方法的安全性和幂等性对API设计至关重要:
安全性:不会修改资源的请求方法(GET、HEAD、OPTIONS)
// 安全方法不应修改状态
router.get('/api/config', async (ctx) => {
// 错误示范:在GET中修改数据
// await updateLastAccessTime();
ctx.body = await getConfig();
});
幂等性:多次执行效果相同的操作(GET、PUT、DELETE、HEAD、OPTIONS)
// 幂等方法示例
router.put('/api/counter', async (ctx) => {
// 无论执行多少次,结果都是counter=5
await setCounter(5);
ctx.body = { value: 5 };
});
方法的选择原则
设计RESTful API时应根据操作语义选择合适的方法:
- 获取数据 - GET
- 创建资源 - POST(客户端不知道URI)或PUT(客户端指定URI)
- 完整更新 - PUT
- 部分更新 - PATCH
- 删除 - DELETE
- 元数据查询 - HEAD
- 跨域预检 - OPTIONS
错误示范:
// 错误:用GET执行删除操作
router.get('/api/users/delete/:id', async (ctx) => {
await User.deleteOne({ _id: ctx.params.id });
ctx.redirect('/users');
});
正确做法:
// 正确:使用DELETE方法
router.delete('/api/users/:id', async (ctx) => {
await User.deleteOne({ _id: ctx.params.id });
ctx.status = 204;
});
方法的状态码对应
不同方法成功执行时应返回适当的状态码:
- GET:200 OK(成功)或304 Not Modified(缓存有效)
- POST:201 Created(资源创建)或202 Accepted(异步处理)
- PUT:200 OK或204 No Content
- PATCH:200 OK或204 No Content
- DELETE:204 No Content
- HEAD:200 OK(不返回内容)
- OPTIONS:204 No Content或200 OK(包含Allow头)
router.post('/api/async-jobs', async (ctx) => {
const job = createAsyncJob(ctx.request.body);
ctx.status = 202; // 异步处理中
ctx.body = {
jobId: job.id,
statusUrl: `/api/jobs/${job.id}`
};
});
方法的缓存行为
不同HTTP方法的缓存特性差异显著:
GET请求可被缓存:
router.get('/api/products', async (ctx) => {
const products = await getProducts();
ctx.set('Cache-Control', 'public, max-age=3600');
ctx.body = products;
});
POST请求通常不应缓存:
router.post('/api/orders', async (ctx) => {
const order = await createOrder(ctx.request.body);
ctx.set('Cache-Control', 'no-store');
ctx.status = 201;
ctx.body = order;
});
方法的Content-Type处理
不同方法对请求体的处理方式不同:
POST/PUT/PATCH通常需要内容类型:
router.post('/api/upload', async (ctx) => {
if (!ctx.is('multipart/form-data')) {
ctx.throw(415, 'Unsupported Media Type');
}
const file = ctx.request.files.file;
// 处理上传...
});
GET请求通常不带请求体:
router.get('/api/search', async (ctx) => {
// 参数应通过查询字符串传递
const { q } = ctx.query;
const results = await search(q);
ctx.body = results;
});
方法的性能考量
不同HTTP方法对性能的影响:
GET请求最轻量:
// 简单GET处理
router.get('/api/status', (ctx) => {
ctx.body = { status: 'ok', timestamp: Date.now() };
});
POST/PUT请求需要更多处理:
router.post('/api/data', async (ctx) => {
// 验证数据
const { error } = validateData(ctx.request.body);
if (error) ctx.throw(400, error.message);
// 处理数据
const result = await processData(ctx.request.body);
// 响应
ctx.status = 201;
ctx.body = result;
});
方法的版本兼容
API演进时方法语义应保持稳定:
不推荐修改现有方法语义:
// v1: GET返回用户列表
router.get('/api/users', getUsers);
// v2错误示范:修改GET语义为返回活跃用户
router.get('/api/users', getActiveUsers);
// v2正确做法:添加新端点或使用查询参数
router.get('/api/users', (ctx) => {
const activeOnly = ctx.query.active === 'true';
return activeOnly ? getActiveUsers() : getUsers();
});
方法的测试策略
针对不同方法需要不同的测试重点:
GET方法测试示例:
describe('GET /api/products', () => {
it('should return all products', async () => {
const res = await request(app)
.get('/api/products')
.expect(200);
expect(res.body).to.be.an('array');
});
});
POST方法测试示例:
describe('POST /api/products', () => {
it('should create a new product', async () => {
const newProduct = { name: 'Test', price: 100 };
const res = await request(app)
.post('/api/products')
.send(newProduct)
.expect(201);
expect(res.body).to.have.property('id');
});
});
方法的错误处理
不同方法需要特定的错误响应:
GET资源不存在:
router.get('/api/users/:id', async (ctx) => {
const user = await User.findById(ctx.params.id);
if (!user) ctx.throw(404, 'User not found');
ctx.body = user;
});
POST数据验证失败:
router.post('/api/users', async (ctx) => {
const { error } = userSchema.validate(ctx.request.body);
if (error) ctx.throw(400, error.details[0].message);
const user = new User(ctx.request.body);
await user.save();
ctx.status = 201;
ctx.body = user;
});
方法的幂等性实现
确保PUT/DELETE等方法的幂等性:
PUT幂等实现:
router.put('/api/settings', async (ctx) => {
// 无论调用多少次,结果相同
await Settings.updateOne(
{},
{ $set: ctx.request.body },
{ upsert: true }
);
ctx.body = await Settings.findOne();
});
DELETE幂等处理:
router.delete('/api/temp-data/:id', async (ctx) => {
// 无论资源是否存在都返回204
await TempData.deleteOne({ _id: ctx.params.id });
ctx.status = 204;
});
方法的Content Negotiation
根据Accept头支持不同响应格式:
GET支持JSON和XML:
router.get('/api/books', async (ctx) => {
const books = await Book.find();
if (ctx.accepts('json')) {
ctx.type = 'json';
ctx.body = books;
} else if (ctx.accepts('xml')) {
ctx.type = 'xml';
ctx.body = generateXML(books);
} else {
ctx.throw(406, 'Not Acceptable');
}
});
方法的速率限制
对不同方法实施不同限流策略:
POST严格限流:
const postLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100 // 每IP最多100次POST请求
});
router.post('/api/comments', postLimiter, async (ctx) => {
// 处理评论提交
});
GET宽松限流:
const getLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 1000 // 每IP最多1000次GET请求
});
router.get('/api/public-data', getLimiter, async (ctx) => {
// 返回公开数据
});
方法的安全防护
针对不同方法实施安全措施:
防止CSRF(通常保护POST/PUT/PATCH/DELETE):
app.use(koaCsrf());
router.post('/api/transfer', async (ctx) => {
// 自动验证CSRF令牌
await processTransfer(ctx.request.body);
ctx.status = 200;
});
GET请求的XSS防护:
router.get('/api/search', async (ctx) => {
const query = xssFilter(ctx.query.q);
const results = await search(query);
ctx.body = results;
});
方法的实时通信
结合WebSocket时的方法使用:
HTTP初始化,WebSocket持续通信:
router.post('/api/chat', async (ctx) => {
const chat = await createChat(ctx.request.body);
ctx.status = 201;
ctx.body = {
chatId: chat.id,
wsUrl: `/ws/chat/${chat.id}`
};
});
方法的监控统计
记录不同方法的访问指标:
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const duration = Date.now() - start;
statsd.increment(`http.${ctx.method}.${ctx.status}`);
statsd.timing(`http.${ctx.method}`, duration);
});
router.get('/api/metrics', async (ctx) => {
ctx.body = {
counts: await getMethodCounts(),
timings: await getMethodTimings()
};
});
方法的文档生成
基于方法定义自动生成API文档:
/**
* @api {get} /api/users/:id 获取用户信息
* @apiName GetUser
* @apiGroup User
*/
router.get('/api/users/:id', getUser);
/**
* @api {post} /api/users 创建用户
* @apiName CreateUser
* @apiGroup User
*/
router.post('/api/users', createUser);
方法的代理转发
在网关层处理不同方法的转发:
router.all('/api/*', async (ctx) => {
const target = determineTarget(ctx.method, ctx.path);
const response = await proxyRequest(ctx.method, target, ctx);
ctx.status = response.status;
ctx.body = response.data;
});
方法的灰度发布
根据方法实现不同的发布策略:
router.get('/api/new-feature', async (ctx) => {
if (shouldEnableNewFeature(ctx)) {
ctx.body = await newFeatureImplementation();
} else {
ctx.body = await legacyImplementation();
}
});
方法的A/B测试
针对不同方法实施差异化测试:
router.post('/api/checkout', async (ctx) => {
const variant = getABTestVariant(ctx);
if (variant === 'B') {
await newCheckoutFlow(ctx.request.body);
} else {
await legacyCheckoutFlow(ctx.request.body);
}
ctx.status = 200;
});
方法的流量复制
将生产流量复制到测试环境:
const shadowRouter = new Router();
shadowRouter.post('/api/orders', async (ctx) => {
// 异步复制到测试环境
shadowRequest('POST',
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
上一篇:大型项目路由拆分方案