From CORS multi-permission errors to Cloudflare cache hit issue

一次完整的 Nginx + FastAPI TTS 服务排错记录

分享
From CORS multi-permission errors to Cloudflare cache hit issue

最近为了保证Worder的英文朗读质量,我部署一个 TTS(Text-to-Speech)API接口微服务,并通过 Cloudflare 做加速缓存。一路踩坑的经历非常典型:

  • CORS 报错 MultipleAllowOriginValues
  • Cloudflare 免费套餐下,页面规则/缓存规则无法缓存 API → cf-cache-status: BYPASS
  • 本地缓存 x-cache: HIT,但 CDN 不命中
  • blob URL 出现 206 Partial Content

这篇文章记录完整排查过程和最终可行方案。


1. 背景

  • 前端站点:https://worder.me
  • TTS API:https://worder.me/api/tts?text=xxx&lang=en&format=mp3
  • 后端:Python FastAPI
  • 前置:Nginx 反向代理 + Cloudflare CDN

目标:让 TTS 结果在 Cloudflare 全站缓存(HIT),减轻服务器压力。


2. CORS 报错 —— MultipleAllowOriginValues

前端播放音频报错:

Cross-Origin Resource Sharing error: MultipleAllowOriginValues

原因

  • FastAPI 添加了 CORS 中间件
  • Nginx 同时设置了 CORS 响应头

浏览器收到两个 Access-Control-Allow-Origin,报错。

解决

  • FastAPI 注释掉 CORS 中间件
  • Nginx 统一管理 CORS:
location /api/ {
    set $cors_origin "";
    if ($http_origin ~* ^https://(www\.)?worder\.me$) {
        set $cors_origin $http_origin;
    }
    add_header Access-Control-Allow-Origin $cors_origin always;
    add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
    add_header Access-Control-Allow-Headers "Content-Type" always;
}

✅ CORS 问题解决。


3. 免费套餐缓存规则 / 页面规则无法解决 BYPASS

尝试:

  • 页面规则:*worder.me/api/tts*
  • 缓存规则:URI 路径开头 /api/tts

但是 Cloudflare 免费套餐的 动态 API + query 参数 仍然显示:

cf-cache-status: BYPASS

原因:

  • 免费套餐无法自定义 Cache Key
  • Cloudflare 默认不缓存带 query 的动态请求
  • 页面规则 / 缓存规则只能缓存静态文件或完全匹配规则

所以必须使用 Worker 强制缓存。


4. Worker 强制缓存解决方案

下面是我实际使用的 Worker 代码:

export default {
  async fetch(request) {
    const url = new URL(request.url);

    // 只处理 TTS API
    if (!url.pathname.startsWith('/api/tts')) {
      return fetch(request);
    }

    // 转发到源站
    const response = await fetch(url.toString(), {
      method: request.method,
      headers: request.headers,
      cf: {
        cacheEverything: true,      // ← 关键:强制 Cloudflare 缓存非静态内容
        cacheTtl: 2592000           // ← 缓存 30 天
      }
    });

    // 创建新响应,移除 Set-Cookie
    const newHeaders = new Headers(response.headers);
    newHeaders.delete('Set-Cookie');

    // 统一返回缓存头
    newHeaders.set('Cache-Control', 'public, max-age=2592000, immutable');

    return new Response(response.body, {
      status: response.status,
      headers: newHeaders
    });
  }
}

说明

  • cacheEverything: true → 即使是动态 API 也会缓存
  • cacheTtl: 2592000 → 缓存 30 天
  • 删除 Set-Cookie → 防止 Cloudflare 拒绝缓存
  • 返回 Cache-Control → 告诉浏览器和 CDN 这是可缓存内容

✅ 使用 Worker 后,cf-cache-status: HIT 正常命中。


5. 验证缓存效果

第一次请求:

cf-cache-status: MISS

第二次请求:

cf-cache-status: HIT 🎉

浏览器端播放正常,TTS 服务器压力大幅下降。


6. blob URL + 206 正常现象

浏览器将音频流转成 blob,206 Partial Content = 支持 Range 请求,可拖动播放,与缓存无关。


7. 总结关键点

  1. CORS 报错 → Nginx + FastAPI 双重设置,保留一个
  2. 免费套餐缓存规则/页面规则 → 动态 API 仍 BYPASS
  3. Worker 强制缓存 → cacheEverything + cacheTtl + Cache-Control
  4. 删除 Set-Cookie → 避免缓存被拒
  5. blob + 206 → 正常现象

🎉 最终效果

  • CORS OK
  • Cloudflare HIT 成功
  • TTS 服务压力降低
  • 全球 CDN 加速
  • 浏览器播放顺畅

阅读更多

为了平凡的生活

为了平凡的生活

为了翻拣垃圾的孩子,和他们的梦想。 为了那些枯萎的树木, 为了那些如纸牌般倒塌的危楼。 为了那些安眠药, 和那些漫长而失眠的夜晚。 为了那些被困在原地的人, 为了改变那些腐朽而停滞的头脑。 为了那些被加速的人生, 为了恐慌,焦虑,和没有尽头的比较。 为了那些被迫接受的规则, 为了那些被强加的指令, 为了那些看不见的手, 我们成长, 成功, 不断向前。 加速学习, 加速工作, 加速赚钱, 加速消费, 加速体验, 加速衰老。 好像停下来, 就会落在后面。 我想慢下来。 想要一份普通的工作, 不用害怕失去。 想让孩子好好长大, 让老人慢慢变老。 想在傍晚回家, 吃一顿晚饭。 想让夜晚只是夜晚, 而不是焦虑、失眠和安眠药。 想让树长在那里, 而不是倒在推土机经过的地方。 想让房子能够站得住, 住进去,可以安心睡觉。 想让一个人活着, 不需要不停证明自己活得有价值。 我们曾经以为, 生活应该越来越精彩。 后来发现, 真正难得的是它能够保持正常。 没有突然坍塌的房子, 没有突然失去的工作, 没有突然降临的疾病,