308 永久重定向:API 版本迁移(/api/v1)时防 POST 请求丢失

一句话:308 Permanent Redirect是HTTP状态码,表示请求的资源已永久移动到新URL。与301/302不同,308会保持请求方法不变,防止POST请求丢失。

1. HTTP重定向基础

1.1 什么是重定向?


graph LR

    A[客户端] -->|请求| B[服务器]

    B -->|308重定向| C[新URL]

    C -->|请求| D[新服务器]

    style B fill:#e1f5fe

重定向:服务器告诉客户端请求的资源已移动到新URL。

1.2 重定向状态码对比

状态码名称方法保持用途
301Moved Permanently可能改变永久移动
302Found可能改变临时移动
303See Other改为GET查看其他
307Temporary Redirect保持临时移动
308Permanent Redirect保持永久移动

2. 308 详解

2.1 什么是308?

 
# 示例响应
 
HTTP/1.1 308 Permanent Redirect
 
Location: https://example.com/new-url
 

2.2 308 vs 301

 
# POST → POST(浏览器行为)
 

3. API版本迁移

3.1 为什么需要308?


graph TD

    A[API版本迁移] --> B[旧版本 /api/v1]

    A --> C[新版本 /api/v2]

    B --> D[308重定向]

    D --> C

    style D fill:#e8f5e8

问题

  • 旧版本API需要废弃

  • 客户端可能还在使用旧版本

  • 需要平滑迁移

3.2 FastAPI实现

 
from fastapi import FastAPI, Request, Response
 
from fastapi.responses import RedirectResponse
 
app = FastAPI()
 
# 旧版本端点
 
@app.post("/api/v1/users/")
 
async def create_user_v1(user: UserCreate):
 
    """v1版本的创建用户"""
 
    return create_user(user)
 
# 新版本端点
 
@app.post("/api/v2/users/")
 
async def create_user_v2(user: UserCreateV2):
 
    """v2版本的创建用户"""
 
    return create_user_v2(user)
 
# 308重定向中间件
 
@app.middleware("http")
 
async def redirect_v1_to_v2(request: Request, call_next):
 
    """将v1请求重定向到v2"""
 
    if request.url.path.startswith("/api/v1/"):
 
        # 构建新URL
 
        new_path = request.url.path.replace("/api/v1/", "/api/v2/")
 
        new_url = f"{request.url.scheme}:// {request.url.netloc}{new_path}"
 
        # 308重定向
 
        return RedirectResponse(
 
            url=new_url,
 
            status_code=308,
 
            headers={"Location": new_url}
 
        )
 
    return await call_next(request)
 

3.3 路由器实现

 
from fastapi import APIRouter, Request
 
from fastapi.responses import RedirectResponse
 
router = APIRouter()
 
# v1路由器
 
v1_router = APIRouter(prefix="/api/v1")
 
v2_router = APIRouter(prefix="/api/v2")
 
@v1_router.post("/users/")
 
async def create_user_v1(user: UserCreate):
 
    return create_user(user)
 
@v2_router.post("/users/")
 
async def create_user_v2(user: UserCreateV2):
 
    return create_user_v2(user)
 
# 308重定向
 
@v1_router.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
 
async def redirect_v1(request: Request, path: str):
 
    """将所有v1请求重定向到v2"""
 
    new_url = f"/api/v2/{path}"
 
    return RedirectResponse(url=new_url, status_code=308)
 
# 注册路由器
 
app.include_router(v1_router)
 
app.include_router(v2_router)
 

4. 实际案例

4.1 电商平台API迁移

 
# 308重定向配置
 
REDIRECT_RULES = {
 
    "/api/v1/products/": "/api/v2/products/",
 
    "/api/v1/orders/": "/api/v2/orders/",
 
    "/api/v1/users/": "/api/v2/users/",
 
}
 
@app.middleware("http")
 
async def api_redirect_middleware(request: Request, call_next):
 
    """API版本重定向"""
 
    path = request.url.path
 
    for old_path, new_path in REDIRECT_RULES.items():
 
        if path.startswith(old_path):
 
            # 保持查询参数
 
            query = str(request.url.query)
 
            new_url = f"{new_path}?{query}" if query else new_path
 
            return RedirectResponse(
 
                url=new_url,
 
                status_code=308,
 
                headers={"Location": new_url}
 
            )
 
    return await call_next(request)
 

4.2 文档重定向

 
# 新文档:/docs/v2/
 
@app.get("/docs/v1/{path:path}")
 
async def redirect_docs_v1(path: str):
 
    """重定向旧文档到新文档"""
 
    return RedirectResponse(
 
        url=f"/docs/v2/{path}",
 
        status_code=308
 
    )
 

5. 客户端处理

5.1 Python requests

 
import requests
 
# 默认情况下,requests会自动处理重定向
 
response = requests.post("https://api.example.com/api/v1/users/", json={})
 
# 禁用自动重定向
 
response = requests.post(
 
    "https://api.example.com/api/v1/users/",
 
    json={},
 
    allow_redirects=False
 
)
 
print(response.status_code)  # 308
 
print(response.headers['Location'])  # 新URL
 

5.2 JavaScript fetch

 
// fetch默认会跟随重定向
 
const response = await fetch('https://api.example.com/api/v1/users/', {
 
    method: 'POST',
 
    body: JSON.stringify({})
 
});
 
// 禁用自动重定向
 
const response = await fetch('https://api.example.com/api/v1/users/', {
 
    method: 'POST',
 
    body: JSON.stringify({}),
 
    redirect: 'manual'
 
});
 
if (response.status === 308) {
 
    const newUrl = response.headers.get('Location');
 
    // 手动重定向
 
}
 

6. 最佳实践

6.1 渐进式迁移

 
# 阶段1:添加新版本,保持旧版本
 
@app.post("/api/v1/users/")
 
@app.post("/api/v2/users/")
 
async def create_user(user: UserCreate):
 
    return create_user(user)
 
# 阶段2:旧版本返回308
 
@app.post("/api/v1/users/")
 
async def create_user_v1_redirect():
 
    return RedirectResponse(url="/api/v2/users/", status_code=308)
 
# 删除/v1端点
 

6.2 日志记录

 
@app.middleware("http")
 
async def redirect_with_logging(request: Request, call_next):
 
    """带日志的重定向"""
 
    if request.url.path.startswith("/api/v1/"):
 
        logger.info(f"API v1 redirect: {request.url.path}")
 
        # 记录重定向统计
 
        increment_redirect_counter(request.url.path)
 
    return await call_next(request)
 

6.3 监控告警

 
# 监控重定向数量
 
@app.get("/metrics")
 
async def get_metrics():
 
    """获取监控指标"""
 
    return {
 
        "redirect_count": get_redirect_count(),
 
        "redirect_by_path": get_redirect_by_path()
 
    }
 

7. 常见坑点

1. 使用302代替308

 
# 错误:使用302
 
return RedirectResponse(url=new_url, status_code=302)
 
# 正确:使用308
 
return RedirectResponse(url=new_url, status_code=308)
 

2. 忘记保持请求方法

 
# 解决:确保使用308而不是301/302
 

3. 查询参数丢失

 
# 问题:重定向时丢失查询参数
 
new_url = "/api/v2/users/"
 
# 正确:保持查询参数
 
query = str(request.url.query)
 
new_url = f"/api/v2/users/?{query}" if query else "/api/v2/users/"
 

核心要点

 
# 用于API版本迁移
 
from fastapi.responses import RedirectResponse
 
return RedirectResponse(
 
    url="/api/v2/users/",
 
    status_code=308,
 
    headers={"Location": "/api/v2/users/"}
 
)
 
# 保持查询参数
 
query = str(request.url.query)
 
new_url = f"/api/v2/users/?{query}" if query else "/api/v2/users/"
 

速记卡(面试闪卡)

Q1:一句话讲清「308 永久重定向:API 版本迁移(/api/v1)时防 POST 请求丢失」到底是什么?

A:308 永久重定向在 API 版本迁移时保持请求方法不变,防止 POST 变 GET 丢数据。

Q2:题目:API 搬家保 POST(308 Permanent Redirect) —— 怎么理解?

A:旧版 /api/v1 要废弃、客户端还在用,得平滑迁到 /api/v2。就像店铺搬迁,老顾客照常 POST 下单,不能因为搬家用 301 把订单悄悄变成 GET 弄丢。

Q3:思路:308 vs 301/302(method-preserving redirect) —— 怎么理解?

A:301/302 浏览器可能把 POST 改成 GET,请求体和 body 全丢;307/308 严格保持方法。308 是”永久”版,还提示客户端更新书签。选型记住:要保方法用 307 临时、308 永久。

Q4:代码:FastAPI 中间件(RedirectResponse 308) —— 怎么理解?

A:中间件里 if path.startswith("/api/v1/") 拼出新 URL,返回 RedirectResponse(url, status_code=308),务必带 Location 头。别忘了把查询参数 ?x=1 也拼回去,否则 GET 参数丢失。

Q5:实战:渐进迁移与监控(gradual migration) —— 怎么理解?

A:三阶段:先两版共存、再 v1 返回 308、最后删 v1。客户端 requests/fetch 默认跟随,可用 allow_redirects=False 或 redirect:‘manual’ 手动处理。记得打日志监控重定向量。

Q6:核心速记主线有哪些?

  • 题目:API v1→v2 迁移用 308 防 POST 丢失

  • 思路:308/307 保方法,301/302 可能改 GET

  • 代码:RedirectResponse status_code=308,带 Location 与查询参数

  • 实战:渐进三阶段迁移,客户端手动可关

  • 坑点:勿用 302 替代、查询参数要保留

口诀

A:API 搬家不丢单,

308 保法莫改换;

301 改 GET 易翻船,

查询参数一并传。

相关链接