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 加速
  • 浏览器播放顺畅

Read more

間

春节回家,我又见到了我干爹家的三儿子。 他生下来就带着残疾,不能说话,手脚不协调,走路一瘸一拐,嘴角总是挂着口水。小时候干爹干娘怕别人欺负他,教他见人就笑。所以这么多年,不管走到哪,他都是笑着的。 左脚脚尖点地,左手弯着伸不直,走路习惯性靠在路的最右边,紧贴着路沿。我有时候担心他会踩进沟里,想想又觉得,也许他自己知道,这样不容易被人撞到。 那天下午我一个人在村东边路上走,他跟了上来。脸上沾着灰,鼻子里有一团鼻垢,我下意识想帮他弄掉,他偏过头,自己扣了下来,然后转过脸,把手里点着的烟举了举,冲我笑。 他的手指黄黄的,染得很深。后来我知道,小时候有人逗他,教他抽烟,就这么上了瘾,又没有能力自己戒。烟瘾越来越大,有烟就一口气抽完,多的时候一天三包。这两年逢年过节,大家口袋里都装着烟,见面互让,他也学会了凑过去。村里谁家办红白喜事,他都去帮着搬凳子搬椅子,人家给他几根烟,他就高兴。我那半包苏烟,后来进了他的口袋。

折叠时间

折叠时间

上次坐地铁的时候,我盯着手机看了一眼时间:20:37。等反应过来抬起头,已经是20:52了。十五分钟,就这么没了。 但1月牙疼去看牙医,在椅子上躺着等医生准备器械,那三分钟感觉比一个小时还长。 同样是时间,为什么有时候像沙子一样从指缝溜走,有时候又像琥珀一样凝固住每一秒? 不同的星球,不同的时钟 物理学告诉我们,引力会让时间变慢。在靠近黑洞的地方过一小时,地球上可能已经过了好几年。就像不同重量的球压在一张网上,越重的球把网面压得越深,时间在那里流逝得就越慢。 这个画面一直让我着迷。 后来我想,其实我们每个人的内心世界也像是不同的星球。有些事情对你来说很重要,它就像一颗大质量的星球,把你的时间网压出很深的凹陷。你围绕着它打转,时间在那里变得又浓又稠。 恋爱的时候,一天能想对方好几百次。每一次心跳都被放大,每一个眼神都值得回味。楼下等她的那段时间好像特别"漫长"。 但也有些日子,你就是在重复。起床、上班、吃饭、睡觉。一天天像复制粘贴一样过去了,回头看,好像什么都没留下。 大象和蚂蚁的一秒钟

思考

思考

在你阅读这篇文章之前,先问自己一个问题:你上一次真正深度思考是什么时候? 我所说的"深度思考",是指遇到一个具体而困难的问题,然后花费好几天时间专注于解决它的那种状态。 你的答案是什么? * a) 经常如此 * b) 从来没有 * c) 介于两者之间 如果你的答案是 (a) 或 (b),这篇文章可能不适合你。但如果像我一样,你的答案是 (c),那么这篇文章或许能引起你的共鸣,至少让你知道,你并不孤单。 首先声明:这篇文章没有答案,甚至没有建议。它只是我最近几个月内心感受的一次宣泄。 建造者与思考者 我相信我的性格建立在两个主要特质之上: 1. 建造者(渴望创造、交付和务实) 2. 思考者(需要深度、持久的智力挑战) 建造者这一面很容易理解,它追求速度和实用性。这是我渴望将"想法"转化为"现实&