HTTP 错误排查实战案例库

15 个真实场景案例,每个案例包含问题描述、排查步骤、解决方案和预防措施

15
实战案例
11
错误类型
60+
排查步骤
100%
实战验证

📊 HTTP错误分类与排查流程

flowchart TD A[收到HTTP响应] --> B{状态码首位} B -->|4xx| C[客户端错误] B -->|5xx| D[服务器错误] B -->|2xx| E[成功 ✅] B -->|3xx| F[重定向] C --> C1[4xx错误一览] C1 --> C1a["400 Bad Request
请求语法错误"] C1 --> C1b["401 Unauthorized
需要认证"] C1 --> C1c["403 Forbidden
禁止访问"] C1 --> C1d["404 Not Found
资源不存在"] C1 --> C1e["429 Too Many Requests
请求过频"] D --> D1[5xx错误一览] D1 --> D1a["500 Internal Server Error
服务器内部错误"] D1 --> D1b["502 Bad Gateway
网关错误"] D1 --> D1c["503 Service Unavailable
服务不可用"] D1 --> D1d["504 Gateway Timeout
网关超时"] C1a --> G1[检查请求URL
验证参数格式] C1b --> G2[检查Authorization
验证Token/凭证] C1c --> G3[检查权限配置
确认资源访问策略] C1d --> G4[检查URL路径
确认资源是否存在] C1e --> G5[实现请求限流
使用指数退避] D1a --> H1[检查服务器日志
定位异常堆栈] D1b --> H2[检查上游服务
确认网关配置] D1c --> H3[检查服务状态
确认服务可用性] D1d --> H4[增加超时时间
检查网络连通性]

案例 1:API 请求返回 400 Bad Request

HTTP 400

问题背景

某电商平台在用户提交订单时,前端调用 POST /api/orders 接口,偶尔返回 400 错误,用户无法完成下单。

错误现象

  • 部分用户提交订单时返回 400 Bad Request
  • 错误不是必现,约 10% 的请求失败
  • 错误请求的响应体:{"error": "Invalid request body", "details": "JSON parse error"}

排查步骤

  1. 检查请求日志:查看 nginx 和后端应用日志,确认 400 错误的具体请求内容
  2. 对比正常和失败请求:发现失败请求的 Content-Type 为 text/plain 而非 application/json
  3. 检查前端代码:发现某些情况下 fetch 请求未正确设置 headers
  4. 验证请求体格式:部分请求的 JSON 字符串包含未转义的特殊字符

根本原因

  • 前端代码中,当用户从剪贴板粘贴商品备注时,某些特殊字符(如换行符、引号)未正确转义
  • 部分旧版本浏览器对 fetch API 的 headers 处理存在兼容性问题

解决方案

// 修复前端代码,确保正确设置 Content-Type 和转义特殊字符
async function submitOrder(orderData) {
    // 转义特殊字符
    const sanitizedData = JSON.stringify(orderData)
        .replace(/\n/g, '\\n')
        .replace(/"/g, '\\"');
    
    const response = await fetch('/api/orders', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        },
        body: sanitizedData
    });
    
    if (!response.ok) {
        const error = await response.json();
        throw new Error(error.details || '订单提交失败');
    }
    
    return response.json();
}

️ 预防措施

  • 前端添加输入验证和 sanitization 层
  • 后端添加更详细的 400 错误响应信息,帮助定位问题
  • 添加请求日志采样,定期分析 400 错误模式
  • 使用 TypeScript 等类型系统减少数据格式错误

案例 2:用户登录后仍返回 401 Unauthorized

HTTP 401

问题背景

某 SaaS 平台用户反馈登录成功后,访问受保护页面时仍提示"未授权",需要重新登录。

错误现象

  • 用户登录成功,获取到 JWT token
  • 访问/api/dashboard 等受保护接口时返回 401
  • 响应头:WWW-Authenticate: Bearer error="invalid_token"
  • 问题在用户刷新页面后必现

排查步骤

  1. 检查 token 存储:发现 token 存储在 localStorage,刷新后正确读取
  2. 检查请求头:发现 Authorization 头格式为"JWT {token}"而非"Bearer {token}"
  3. 检查后端验证逻辑:后端期望 Bearer schema,前端发送 JWT schema
  4. 检查 token 有效期:token 未过期,问题与有效期无关

根本原因

前端代码中 Authorization 头的 schema 写错,使用了"JWT"而非标准的"Bearer"。

解决方案

// 修复 Authorization 头格式
function getAuthHeaders() {
    const token = localStorage.getItem('access_token');
    return {
        'Authorization': `Bearer ${token}`, // 使用 Bearer 而非 JWT
        'Content-Type': 'application/json'
    };
}

// 添加 token 刷新逻辑
async function refreshToken() {
    const refreshToken = localStorage.getItem('refresh_token');
    const response = await fetch('/api/auth/refresh', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ refresh_token: refreshToken })
    });
    
    if (response.ok) {
        const data = await response.json();
        localStorage.setItem('access_token', data.access_token);
        return data.access_token;
    }
    
    // Token 刷新失败,跳转到登录页
    window.location.href = '/login';
}

️ 预防措施

  • 使用 axios 等 HTTP 客户端库的拦截器统一管理认证头
  • 添加 401 响应的全局处理,自动刷新 token 或跳转登录
  • 在开发环境添加详细的认证错误日志
  • 编写集成测试覆盖认证流程

案例 3:管理员访问接口返回 403 Forbidden

HTTP 403

问题背景

某后台管理系统,管理员账号登录后访问用户管理接口 DELETE /api/users/:id,返回 403 Forbidden。

错误现象

  • 管理员账号可以正常登录
  • 访问 GET 接口正常,但 DELETE 接口返回 403
  • 响应体:{"error": "Forbidden", "message": "Insufficient permissions"}
  • 同一账号在其他环境(测试环境)工作正常

排查步骤

  1. 检查用户角色:确认账号确实有 admin 角色
  2. 检查权限配置:发现生产环境权限配置文件中 DELETE /api/users/*需要"admin:write"权限
  3. 检查 token 中的权限声明:JWT token 中只有"admin:read"权限
  4. 对比环境配置:测试环境权限配置较宽松,生产环境更严格

根本原因

生产环境的 RBAC 权限配置与测试环境不一致,管理员账号缺少"admin:write"权限声明。

解决方案

// 后端权限配置修复(Node.js + Express 示例)
const permissions = {
    'admin:read': ['GET'],
    'admin:write': ['GET', 'POST', 'PUT', 'DELETE'],
    'user:read': ['GET'],
    'user:write': ['GET', 'PUT']
};

function checkPermission(requiredPermission, method) {
    const allowedMethods = permissions[requiredPermission];
    if (!allowedMethods || !allowedMethods.includes(method)) {
        return false;
    }
    return true;
}

// 前端请求时确保携带正确的权限 scope
// 或在登录时请求包含完整权限的 token

️ 预防措施

  • 使用基础设施即代码(IaC)确保各环境配置一致
  • 添加权限配置的自动化测试
  • 在 CI/CD 流程中添加配置差异检查
  • 建立权限变更的审批流程

案例 4:前端路由刷新后返回 404 Not Found

HTTP 404

问题背景

某 SPA 应用使用 React Router,用户直接访问 https://example.com/users/123 或刷新页面时返回 404。

错误现象

  • 从首页点击导航到/users/123 正常
  • 直接在浏览器地址栏输入/users/123 或刷新页面时返回 404
  • nginx 错误日志:open() "/var/www/html/users/123" failed (2: No such file or directory)

排查步骤

  1. 检查前端路由配置:React Router 使用 BrowserHistory 模式
  2. 检查 nginx 配置:发现没有配置 fallback 到 index.html
  3. 理解 SPA 路由原理:前端路由需要后端将所有未知路径重写到 index.html
  4. 验证修复:添加 try_files 配置后问题解决

根本原因

SPA 应用的前端路由(如 React Router 的 BrowserRouter)需要后端服务器将所有未知路径重写到 index.html,由前端 JavaScript 处理路由。nginx 缺少此配置导致直接访问非根路径时返回 404。

解决方案

# nginx 配置修复
server {
    listen 80;
    server_name example.com;
    root /var/www/html;
    index index.html;

    location / {
        # 尝试文件,不存在则返回 index.html 让前端路由处理
        try_files $uri $uri/ /index.html;
    }

    # API 请求代理到后端
    location /api/ {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

️ 预防措施

  • 在部署文档中明确 SPA 应用的服务器配置要求
  • 使用 Docker 等容器化部署,将配置打包在镜像中
  • 添加部署后的自动化测试,验证所有路由可访问
  • 考虑使用 HashRouter 模式(URL 带#),无需后端配置

案例 5:POST 请求返回 405 Method Not Allowed

HTTP 405

问题背景

某 API 网关配置后,客户端发送 POST 请求到/api/data 返回 405 Method Not Allowed。

错误现象

  • GET /api/data 正常工作
  • POST /api/data 返回 405
  • 响应头包含:Allow: GET, HEAD, OPTIONS
  • 后端服务实际支持 POST 方法

排查步骤

  1. 检查后端服务:确认后端代码中 POST 路由已定义
  2. 检查 API 网关配置:发现网关的 location 块中限制了允许的方法
  3. 检查 CORS 预检:OPTIONS 请求正常,但实际 POST 被拦截
  4. 查看网关日志:确认 405 由网关返回,未到达后端

根本原因

nginx 网关配置中使用了 limit_except 指令限制了允许的方法,但未包含 POST。

解决方案

# 修复前的 nginx 配置(错误)
location /api/ {
    limit_except GET HEAD OPTIONS {
        deny all;
    }
    proxy_pass http://backend;
}

# 修复后的 nginx 配置
location /api/ {
    # 允许所有常用方法
    limit_except GET POST PUT DELETE PATCH HEAD OPTIONS {
        deny all;
    }
    proxy_pass http://backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

# 或者完全移除 limit_except,由后端控制方法限制

️ 预防措施

  • 在网关配置中使用更宽松的方法限制,由后端细粒度控制
  • 添加 API 文档,明确每个端点支持的方法
  • 使用 OpenAPI/Swagger 规范定义 API,自动生成文档和测试
  • 添加端到端测试覆盖所有 HTTP 方法

案例 6:大文件上传返回 408 Request Timeout

HTTP 408

问题背景

某文件管理系统,用户上传超过 50MB 的文件时,上传过程中连接被断开,返回 408 Request Timeout。

错误现象

  • 小文件(<10MB)上传正常
  • 大文件上传到约 60% 进度时连接断开
  • nginx 错误日志:client request timed out
  • 后端应用日志中无请求记录(请求未到达后端)

排查步骤

  1. 检查 nginx 超时配置:发现 client_body_timeout 为 60 秒
  2. 计算上传时间:50MB 文件在 1Mbps 上传速度下需要约 400 秒
  3. 检查客户端配置:前端未设置上传进度和超时处理
  4. 检查网络条件:部分用户网络较慢,上传时间长

根本原因

nginx 的 client_body_timeout 设置过短(60 秒),慢速网络用户上传大文件时超过此时间导致连接被断开。

解决方案

# nginx 配置优化
http {
    # 增加客户端请求体超时时间(针对大文件上传)
    client_body_timeout 600s;  # 10 分钟
    
    # 增加客户端请求头超时时间
    client_header_timeout 60s;
    
    # 增加发送响应超时时间
    send_timeout 600s;
    
    # 增加客户端请求体最大大小
    client_max_body_size 500M;  # 允许最大 500MB
    
    server {
        location /upload {
            # 针对上传路径的单独配置
            client_body_timeout 1800s;  # 30 分钟
            client_max_body_size 500M;
            proxy_pass http://backend;
        }
    }
}

# 前端添加上传进度和重试逻辑
function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);
    
    fetch('/upload', {
        method: 'POST',
        body: formData,
        // 注意:fetch 本身不支持上传进度,需使用 XMLHttpRequest
    });
}

// 使用 XMLHttpRequest 获取上传进度
function uploadFileWithProgress(file) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', '/upload', true);
        
        xhr.upload.onprogress = (event) => {
            if (event.lengthComputable) {
                const percent = (event.loaded / event.total) * 100;
                console.log(`上传进度:${percent.toFixed(2)}%`);
            }
        };
        
        xhr.onload = () => {
            if (xhr.status === 200) {
                resolve(xhr.response);
            } else {
                reject(new Error(`上传失败:${xhr.status}`));
            }
        };
        
        xhr.onerror = () => reject(new Error('网络错误'));
        xhr.ontimeout = () => reject(new Error('上传超时'));
        
        xhr.timeout = 1800000; // 30 分钟超时
        xhr.send(file);
    });
}

️ 预防措施

  • 根据业务需求合理设置超时时间
  • 大文件上传使用分片上传,避免单次请求时间过长
  • 前端添加上传进度显示和断点续传功能
  • 监控上传失败率,及时发现配置问题

案例 7:高频 API 调用返回 429 Too Many Requests

HTTP 429

问题背景

某数据同步服务在批量导入数据时,调用第三方 API 频繁收到 429 错误,导致同步失败。

错误现象

  • 批量导入 1000 条数据时,约 30% 的请求返回 429
  • 响应头包含:Retry-After: 60
  • 响应体:{"error": "rate_limit_exceeded", "retry_after": 60}
  • 错误集中在批量操作的开始阶段

排查步骤

  1. 检查 API 限流策略:第三方 API 限制为 100 次/分钟
  2. 分析请求频率:代码中并发发送请求,瞬间超过限制
  3. 检查重试逻辑:收到 429 后立即重试,加剧问题
  4. 查看 API 文档:确认限流规则和推荐的重试策略

根本原因

代码中未实现速率限制和正确的重试策略,并发请求超过第三方 API 的限流阈值,且收到 429 后立即重试导致问题恶化。

解决方案

// 实现带速率限制和指数退避重试的请求函数
class RateLimitedClient {
    constructor(requestsPerMinute = 100) {
        this.requestsPerMinute = requestsPerMinute;
        this.interval = 60000 / requestsPerMinute; // 毫秒
        this.lastRequestTime = 0;
        this.queue = [];
    }
    
    async request(url, options) {
        // 等待速率限制
        await this.throttle();
        
        try {
            const response = await fetch(url, options);
            
            if (response.status === 429) {
                // 从 Retry-After 头获取等待时间
                const retryAfter = response.headers.get('Retry-After') || 60;
                await this.sleep(retryAfter * 1000);
                return this.request(url, options); // 重试
            }
            
            return response;
        } catch (error) {
            // 网络错误,指数退避重试
            throw error;
        }
    }
    
    async throttle() {
        const now = Date.now();
        const waitTime = this.lastRequestTime + this.interval - now;
        
        if (waitTime > 0) {
            await this.sleep(waitTime);
        }
        
        this.lastRequestTime = Date.now();
    }
    
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}

// 使用示例
const client = new RateLimitedClient(100); // 100 次/分钟

async function batchImport(data) {
    const results = [];
    for (const item of data) {
        const response = await client.request('/api/import', {
            method: 'POST',
            body: JSON.stringify(item)
        });
        results.push(await response.json());
    }
    return results;
}

️ 预防措施

  • 在调用外部 API 前仔细阅读限流策略
  • 实现客户端速率限制,主动控制请求频率
  • 实现指数退避重试策略(Exponential Backoff)
  • 添加请求队列,平滑请求峰值
  • 监控 API 调用成功率,设置告警

案例 8:数据库连接失败导致 500 Internal Server Error

HTTP 500

问题背景

某电商网站在高峰期频繁出现 500 错误,用户无法访问商品详情页和下单。

错误现象

  • 错误集中在工作日 10:00-11:00 和 14:00-15:00
  • 错误日志:Error: Connect ETIMEDOUT 10.0.0.5:5432
  • 数据库 CPU 使用率峰值达 95%
  • 应用服务器连接池耗尽

排查步骤

  1. 检查数据库监控:发现慢查询数量激增
  2. 分析慢查询日志:发现某商品列表查询缺少索引
  3. 检查应用连接池:连接池大小 20,高峰期全部占用
  4. 分析业务逻辑:发现 N+1 查询问题,每个商品额外查询分类信息

根本原因

  • 商品列表查询缺少适当索引,导致全表扫描
  • 代码中存在 N+1 查询问题,单次请求产生数十个数据库查询
  • 连接池大小不足以应对高峰期并发

解决方案

-- 1. 添加数据库索引
CREATE INDEX idx_products_category_status ON products(category_id, status);
CREATE INDEX idx_products_created_at ON products(created_at DESC);

-- 2. 优化查询,使用 JOIN 替代 N+1 查询
-- 优化前(N+1 查询)
SELECT * FROM products WHERE category_id = 1;
-- 然后对每个 product 查询 category
SELECT * FROM categories WHERE id = ?;

-- 优化后(单次 JOIN 查询)
SELECT p.*, c.name as category_name 
FROM products p
JOIN categories c ON p.category_id = c.id
WHERE p.category_id = 1;

-- 3. 调整连接池配置(Node.js + pg 示例)
const pool = new Pool({
    host: '10.0.0.5',
    port: 5432,
    database: 'ecommerce',
    user: 'app_user',
    password: process.env.DB_PASSWORD,
    max: 50,              // 最大连接数从 20 增加到 50
    min: 10,              // 最小空闲连接
    idleTimeoutMillis: 30000,
    connectionTimeoutMillis: 5000,
});

️ 预防措施

  • 添加数据库查询性能监控和告警
  • 使用 ORM 时注意 N+1 查询问题,使用 eager loading
  • 定期进行慢查询分析和索引优化
  • 压力测试验证连接池配置是否合理
  • 实现数据库连接健康检查和自动恢复

案例 9:后端服务重启导致 502 Bad Gateway

HTTP 502

问题背景

某微服务架构应用,在后端服务部署重启期间,用户访问返回 502 Bad Gateway。

错误现象

  • 部署期间约 30 秒内大量 502 错误
  • nginx 错误日志:connect() failed (111: Connection refused)
  • 后端服务重启完成后自动恢复
  • 使用滚动部署仍有短暂错误窗口

排查步骤

  1. 检查部署流程:发现先停止旧服务再启动新服务
  2. 检查 nginx 配置:upstream 中只有一个后端实例
  3. 检查健康检查:nginx 未配置主动健康检查
  4. 分析错误时间线:502 错误与服务停止/启动时间完全吻合

根本原因

部署时先停止旧服务再启动新服务,导致短暂的服务不可用窗口。nginx 未配置健康检查和故障转移,所有请求都发往已停止的服务。

解决方案

# 1. nginx upstream 配置优化(多实例 + 健康检查)
upstream backend {
    least_conn;  # 最少连接负载均衡
    
    server 10.0.0.10:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.11:8080 max_fails=3 fail_timeout=30s;
    server 10.0.0.12:8080 max_fails=3 fail_timeout=30s backup;  # 备份服务器
    
    keepalive 32;  # 保持长连接
}

server {
    location / {
        proxy_pass http://backend;
        proxy_connect_timeout 5s;   # 连接超时
        proxy_read_timeout 30s;     # 读取超时
        proxy_send_timeout 30s;     # 发送超时
        
        # 失败重试
        proxy_next_upstream error timeout http_502 http_503 http_504;
        proxy_next_upstream_tries 3;
    }
}

# 2. 使用滚动部署(Kubernetes 示例)
# deployment.yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 最多超出期望副本数
      maxUnavailable: 0  # 部署期间不可用副本数为 0
  
  # 就绪探针,确保新 Pod 就绪后才接收流量
  readinessProbe:
    httpGet:
      path: /health
      port: 8080
    initialDelaySeconds: 5
    periodSeconds: 10
    failureThreshold: 3
  
  # 存活探针
  livenessProbe:
    httpGet:
      path: /health
      port: 8080
    initialDelaySeconds: 30
    periodSeconds: 10

# 3. 添加优雅关闭处理(Node.js 示例)
process.on('SIGTERM', () => {
    console.log('收到 SIGTERM 信号,开始优雅关闭...');
    
    // 停止接收新连接
    server.close(() => {
        console.log('HTTP 服务器已关闭');
        
        // 关闭数据库连接
        db.close(() => {
            console.log('数据库连接已关闭');
            process.exit(0);
        });
    });
    
    // 强制退出(10 秒后)
    setTimeout(() => {
        console.error('优雅关闭超时,强制退出');
        process.exit(1);
    }, 10000);
});

️ 预防措施

  • 使用多实例部署,避免单点故障
  • 配置负载均衡器的健康检查和故障转移
  • 实现滚动部署,确保部署期间服务可用
  • 添加优雅关闭处理,等待在途请求完成
  • 配置就绪探针,确保新实例就绪后才接收流量

案例 10:服务过载返回 503 Service Unavailable

HTTP 503

问题背景

某秒杀活动期间,订单服务返回 503 Service Unavailable,大量用户无法下单。

错误现象

  • 活动开始后 1 分钟内出现大量 503 错误
  • 响应体:{"error": "Service Unavailable", "message": "Server overloaded"}
  • 服务器 CPU 使用率 100%,内存耗尽
  • 应用日志中出现大量"Connection pool exhausted"

排查步骤

  1. 检查服务器资源:CPU、内存、磁盘 IO 均达到上限
  2. 检查应用指标:请求队列长度激增,响应时间超过 10 秒
  3. 分析流量模式:活动开始后 QPS 从 100 激增到 5000
  4. 检查依赖服务:数据库、缓存、消息队列均出现瓶颈

根本原因

  • 未对秒杀活动做容量规划和限流准备
  • 应用无请求队列限制,所有请求都尝试处理导致资源耗尽
  • 数据库连接池耗尽,新请求无法获取连接
  • 缺少服务降级和熔断机制

解决方案

# 1. nginx 限流配置
http {
    # 定义限流区域(按 IP)
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    
    # 定义连接数限制
    limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
    
    server {
        location /api/orders {
            # 请求频率限制
            limit_req zone=api_limit burst=20 nodelay;
            
            # 连接数限制
            limit_conn conn_limit 10;
            
            # 超过限制返回 503
            limit_req_status 503;
            limit_conn_status 503;
            
            proxy_pass http://backend;
        }
    }
}

# 2. 应用层熔断降级(Node.js + Opossum 示例)
const CircuitBreaker = require('opossum');

const options = {
    timeout: 3000,           // 3 秒超时
    errorThresholdPercentage: 50,  // 50% 错误率触发熔断
    resetTimeout: 30000      // 30 秒后尝试恢复
};

const orderBreaker = new CircuitBreaker(createOrder, options);

orderBreaker.fallback(() => {
    // 降级处理:返回排队中状态,建议用户稍后重试
    return {
        status: 'queued',
        message: '订单处理繁忙,请稍后在订单中心查看结果',
        retryAfter: 30
    };
});

orderBreaker.on('open', () => {
    console.log('熔断器打开,服务降级');
    // 发送告警
});

async function createOrder(orderData) {
    // 正常的订单创建逻辑
    return await orderService.create(orderData);
}

# 3. 消息队列削峰
# 将同步下单改为异步处理
async function placeOrder(orderData) {
    // 快速响应,将订单放入消息队列
    await messageQueue.publish('orders', orderData);
    
    return {
        orderId: generateOrderId(),
        status: 'processing',
        message: '订单已提交,处理中...'
    };
}

️ 预防措施

  • 大型活动前进行容量规划和压力测试
  • 实施多层限流(网关层、应用层、依赖服务层)
  • 配置熔断器和降级策略
  • 使用消息队列削峰填谷
  • 建立完善的监控告警体系
  • 准备应急预案和手动降级开关

案例 11:慢查询导致 504 Gateway Timeout

HTTP 504

问题背景

某报表导出功能,用户导出大数据量报表时返回 504 Gateway Timeout。

错误现象

  • 导出小数据量报表(<1000 条)正常
  • 导出大数据量报表(>10000 条)时返回 504
  • nginx 错误日志:upstream timed out (110: Connection timed out)
  • 后端应用仍在处理,但 nginx 已断开连接

排查步骤

  1. 检查 nginx 超时配置:proxy_read_timeout 为 60 秒
  2. 分析后端处理时间:大数据量报表需要 2-5 分钟
  3. 检查数据库查询:发现复杂聚合查询无优化
  4. 评估业务需求:报表导出确实需要较长时间

根本原因

报表导出是长时间运行任务,但 nginx 的 proxy_read_timeout 设置过短(60 秒),导致后端尚未完成处理 nginx 就返回 504。

解决方案

# 方案 1:调整 nginx 超时配置(针对导出路径)
server {
    location /api/export {
        # 增加超时时间
        proxy_read_timeout 300s;   # 5 分钟
        proxy_send_timeout 300s;
        proxy_connect_timeout 10s;
        
        # 关闭请求缓冲,支持流式传输
        proxy_buffering off;
        proxy_request_buffering off;
        
        proxy_pass http://backend;
    }
}

# 方案 2:异步导出(推荐)
# 前端请求
async function requestExport(reportParams) {
    const response = await fetch('/api/export/request', {
        method: 'POST',
        body: JSON.stringify(reportParams)
    });
    
    const { exportId, estimatedTime } = await response.json();
    
    // 轮询导出状态
    return pollExportStatus(exportId);
}

async function pollExportStatus(exportId) {
    const maxAttempts = 60; // 最多轮询 60 次
    const interval = 5000;   // 每 5 秒轮询一次
    
    for (let i = 0; i < maxAttempts; i++) {
        const response = await fetch(`/api/export/status/${exportId}`);
        const status = await response.json();
        
        if (status.status === 'completed') {
            // 导出完成,下载文件
            window.location.href = `/api/export/download/${exportId}`;
            return;
        }
        
        if (status.status === 'failed') {
            throw new Error('导出失败:' + status.error);
        }
        
        // 等待后继续轮询
        await new Promise(r => setTimeout(r, interval));
    }
    
    throw new Error('导出超时');
}

# 后端处理
@app.route('/api/export/request', methods=['POST'])
def request_export():
    # 创建导出任务,放入后台队列
    export_id = generate_export_id()
    task_queue.enqueue('generate_report', export_id, request.json)
    
    return jsonify({
        'exportId': export_id,
        'estimatedTime': 120  # 预计 2 分钟
    })

@app.route('/api/export/status/')
def export_status(export_id):
    task = task_queue.get_task(export_id)
    return jsonify({
        'status': task.status,  # pending/processing/completed/failed
        'progress': task.progress,
        'error': task.error
    })

️ 预防措施

  • 长时间运行任务使用异步处理模式
  • 提供任务状态查询接口,前端轮询或 WebSocket 推送
  • 针对不同接口设置合理的超时时间
  • 优化数据库查询,添加索引和缓存
  • 大数据量导出支持分页或流式处理

案例 12:JWT Token 过期导致批量 401 错误

HTTP 401

问题背景

某移动 App 用户反馈使用一段时间后,所有操作都提示"登录已过期",需要重新登录。

错误现象

  • 用户登录后正常使用约 2 小时
  • 之后所有 API 请求返回 401,响应体:{"error": "token_expired"}
  • 重新登录后恢复正常,但 2 小时后问题复现
  • 后端日志显示 token 验证失败:Token has expired

排查步骤

  1. 检查 token 配置:发现 access_token 有效期设置为 2 小时
  2. 检查刷新机制:前端代码中缺少 token 刷新逻辑
  3. 检查 refresh_token:后端支持 refresh_token,但前端未使用
  4. 分析用户流程:用户长时间使用 App 时 token 过期无自动刷新

根本原因

access_token 有效期较短(2 小时),但前端未实现自动刷新机制。用户长时间使用 App 时 token 过期,导致所有请求返回 401。

解决方案

// 前端实现 token 自动刷新(axios 拦截器示例)
import axios from 'axios';

const api = axios.create({
    baseURL: '/api',
    timeout: 10000
});

// 请求拦截器:添加 token
api.interceptors.request.use(config => {
    const token = localStorage.getItem('access_token');
    if (token) {
        config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
});

// 响应拦截器:处理 401 错误,自动刷新 token
let isRefreshing = false;
let failedQueue = [];

const processQueue = (error, token = null) => {
    failedQueue.forEach(prom => {
        if (error) {
            prom.reject(error);
        } else {
            prom.resolve(token);
        }
    });
    failedQueue = [];
};

api.interceptors.response.use(
    response => response,
    async error => {
        const originalRequest = error.config;
        
        if (error.response?.status === 401 && !originalRequest._retry) {
            if (isRefreshing) {
                return new Promise((resolve, reject) => {
                    failedQueue.push({ resolve, reject });
                })
                .then(token => {
                    originalRequest.headers.Authorization = `Bearer ${token}`;
                    return api(originalRequest);
                })
                .catch(err => Promise.reject(err));
            }
            
            originalRequest._retry = true;
            isRefreshing = true;
            
            try {
                const refreshToken = localStorage.getItem('refresh_token');
                const response = await axios.post('/api/auth/refresh', {
                    refresh_token: refreshToken
                });
                
                const { access_token } = response.data;
                localStorage.setItem('access_token', access_token);
                
                processQueue(null, access_token);
                
                originalRequest.headers.Authorization = `Bearer ${access_token}`;
                return api(originalRequest);
            } catch (refreshError) {
                processQueue(refreshError, null);
                // 刷新失败,跳转到登录页
                localStorage.removeItem('access_token');
                localStorage.removeItem('refresh_token');
                window.location.href = '/login';
                return Promise.reject(refreshError);
            } finally {
                isRefreshing = false;
            }
        }
        
        return Promise.reject(error);
    }
);

️ 预防措施

  • 实现 access_token + refresh_token 双 token 机制
  • 前端添加 token 过期自动刷新逻辑
  • 使用 HTTP 拦截器统一处理 401 错误
  • 设置合理的 token 有效期(access_token 短,refresh_token 长)
  • 添加 token 过期前的主动刷新(如在过期前 5 分钟刷新)

案例 13:前端跨域请求返回 403 CORS 错误

HTTP 403 (CORS)

问题背景

某 Web 应用前端部署在 https://app.example.com,后端 API 在 https://api.example.com,前端调用 API 时浏览器控制台报 CORS 错误。

错误现象

  • 浏览器控制台错误:Access to fetch at 'https://api.example.com/data' from origin 'https://app.example.com' has been blocked by CORS policy
  • 网络面板显示 OPTIONS 预检请求返回 403
  • 直接访问 API URL 正常,仅前端调用失败
  • 后端日志显示 OPTIONS 请求被拒绝

排查步骤

  1. 检查后端 CORS 配置:发现只允许了 http://localhost:3000
  2. 检查预检请求:OPTIONS 请求未返回正确的 Access-Control 头
  3. 检查请求头:前端发送了自定义头 Content-Type: application/json,触发预检
  4. 验证修复:添加 app.example.com 到允许来源后问题解决

根本原因

后端 CORS 配置只允许了开发环境的 localhost,未添加生产环境的前端域名。浏览器因同源策略阻止跨域请求。

解决方案

# Node.js + Express CORS 配置
const express = require('express');
const cors = require('cors');
const app = express();

const corsOptions = {
    origin: function (origin, callback) {
        // 允许的来源列表
        const allowedOrigins = [
            'https://app.example.com',
            'https://www.example.com',
            'http://localhost:3000'  // 开发环境
        ];
        
        // 允许无 origin(如移动端 App、curl)
        if (!origin) return callback(null, true);
        
        if (allowedOrigins.indexOf(origin) !== -1) {
            callback(null, true);
        } else {
            callback(new Error('Not allowed by CORS'));
        }
    },
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
    credentials: true,  // 允许携带凭证(cookies)
    maxAge: 86400  // 预检请求缓存 24 小时
};

app.use(cors(corsOptions));

# Nginx CORS 配置
server {
    listen 443 ssl;
    server_name api.example.com;
    
    location / {
        # CORS 头配置
        add_header 'Access-Control-Allow-Origin' 'https://app.example.com' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;
        add_header 'Access-Control-Max-Age' '86400' always;
        
        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://app.example.com';
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS';
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With';
            add_header 'Access-Control-Allow-Credentials' 'true';
            add_header 'Access-Control-Max-Age' '86400';
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' '0';
            return 204;
        }
        
        proxy_pass http://backend;
    }
}

️ 预防措施

  • 使用环境变量管理 CORS 允许来源,区分开发和生产环境
  • 在 CI/CD 流程中添加 CORS 配置检查
  • 使用 API 网关统一管理 CORS 策略
  • 添加 CORS 测试用例,部署前验证
  • 考虑使用同源部署(前端后端同一域名,不同路径)避免 CORS

案例 14:空指针异常导致 500 Internal Server Error

HTTP 500

问题背景

某 Java 后端服务,部分用户访问个人中心时返回 500 错误,错误日志显示 NullPointerException。

错误现象

  • 部分用户访问/user/profile 返回 500
  • 错误日志:NullPointerException at UserProfileService.getProfile(UserProfileService.java:45)
  • 新用户正常,老用户部分失败
  • 错误与特定用户数据相关

排查步骤

  1. 定位错误代码行:第 45 行访问 user.getAddress().getCity()
  2. 分析失败用户数据:这些用户的 address 字段为 null
  3. 追溯数据来源:老用户迁移时部分数据未完整迁移
  4. 检查代码防御:未对可能为 null 的对象做空值检查

根本原因

代码中直接链式调用 user.getAddress().getCity(),但部分老用户数据中 address 为 null,导致空指针异常。

解决方案

// 修复前(有问题)
public UserProfileDTO getProfile(Long userId) {
    User user = userRepository.findById(userId);
    UserProfileDTO dto = new UserProfileDTO();
    dto.setName(user.getName());
    dto.setCity(user.getAddress().getCity());  // 可能 NPE
    return dto;
}

// 修复后(防御式编程)
public UserProfileDTO getProfile(Long userId) {
    User user = userRepository.findById(userId);
    if (user == null) {
        throw new UserNotFoundException(userId);
    }
    
    UserProfileDTO dto = new UserProfileDTO();
    dto.setName(user.getName());
    
    // 空值检查
    if (user.getAddress() != null) {
        dto.setCity(user.getAddress().getCity());
        dto.setDistrict(user.getAddress().getDistrict());
    } else {
        dto.setCity("");
        dto.setDistrict("");
    }
    
    return dto;
}

// 或使用 Java 8 Optional
public UserProfileDTO getProfile(Long userId) {
    User user = userRepository.findById(userId)
        .orElseThrow(() -> new UserNotFoundException(userId));
    
    UserProfileDTO dto = new UserProfileDTO();
    dto.setName(user.getName());
    
    dto.setCity(Optional.ofNullable(user.getAddress())
        .map(Address::getCity)
        .orElse(""));
    
    return dto;
}

️ 预防措施

  • 使用防御式编程,对所有外部输入和可能为 null 的值做空值检查
  • 使用 Optional 等工具类处理可能为空的情况
  • 数据迁移时添加完整性校验
  • 使用静态分析工具(如 SpotBugs)检测潜在 NPE
  • 编写单元测试覆盖边界情况(null 值、空集合等)
  • 使用 @NotNull 等注解进行编译期检查

案例 15:JSON 格式错误导致 400 Bad Request

HTTP 400

问题背景

某 API 接口接收 JSON 请求体,部分客户端请求返回 400 错误,提示"Invalid JSON"。

错误现象

  • 部分 API 请求返回 400 Bad Request
  • 错误信息:{"error": "Invalid JSON", "details": "Unexpected token ' in JSON at position 15"}
  • 错误集中在某个客户端版本
  • 相同请求用 Postman 发送正常

排查步骤

  1. 捕获错误请求:使用日志记录原始请求体
  2. 分析请求体:发现字符串值中包含单引号:{"name": "John's"}
  3. 检查客户端代码:客户端使用字符串拼接构造 JSON,未正确转义
  4. 验证问题:单引号在 JSON 中需要转义或使用双引号

根本原因

客户端代码使用字符串拼接构造 JSON,当数据中包含特殊字符(如单引号、双引号、换行符)时未正确转义,导致 JSON 格式无效。

解决方案

// 错误的客户端代码(字符串拼接)
function createUser(name, email) {
    // 当 name 包含单引号时会产生无效 JSON
    const json = '{"name": "' + name + '", "email": "' + email + '"}';
    return json;
}
// 调用:createUser("John's", "john@example.com")
// 结果:{"name": "John's", "email": "john@example.com"}  // 无效 JSON

// 正确的客户端代码(使用 JSON.stringify)
function createUser(name, email) {
    const data = { name, email };
    const json = JSON.stringify(data);  // 自动处理转义
    return json;
}
// 调用:createUser("John's", "john@example.com")
// 结果:{"name":"John's","email":"john@example.com"}  // 有效 JSON

// 后端添加更详细的错误响应
app.post('/api/users', (req, res) => {
    try {
        const userData = req.body;  // Express 自动解析 JSON
        
        // 验证必填字段
        if (!userData.name || !userData.email) {
            return res.status(400).json({
                error: 'Invalid request',
                details: 'Missing required fields: name, email',
                received: Object.keys(userData)
            });
        }
        
        // 处理业务逻辑
        const user = createUser(userData);
        res.json(user);
        
    } catch (error) {
        if (error instanceof SyntaxError) {
            return res.status(400).json({
                error: 'Invalid JSON',
                details: error.message,
                example: '{"name": "John", "email": "john@example.com"}'
            });
        }
        throw error;
    }
});

️ 预防措施

  • 始终使用 JSON.stringify() 或等效方法序列化 JSON,避免字符串拼接
  • 后端添加详细的 JSON 解析错误信息,帮助客户端定位问题
  • 使用 TypeScript 等类型系统减少数据格式错误
  • 添加请求验证中间件(如 Joi、Zod)
  • 在 API 文档中提供请求示例和格式要求