HTTP/HTTPS 调试噩梦:24 小时深陷 Mixed Content 地狱
我花了 24 小时排查:为什么我的 React 应用在 HTTPS 页面里仍然发 HTTP 请求——明明代码里已经在做转换。最终元凶让我震惊。
---
更新(2025 年 11 月): STRAŦUM 现在已经进入 Private Alpha!本文提到的 9-agent 营销平台正在招募早期测试者。可前往 stratum.chandlernguyen.com 申请,或阅读 完整发布故事。
---
背景:营销平台之旅继续推进
还记得我那篇 9 月文章 吗?我当时说自己一边午睡一边“速通”一个 10-agent 营销平台。4 周时已有 3 个可用 agent,目标在 10/11 月上 alpha。
现在已经到 10 月下旬了,是时候汇报进展。
好消息:
- 平台终于有名字了:STRAŦUM(Intelligence Over Execution)
- 完整品牌规范和设计系统(事实证明做品牌比写代码更费时)
- 10 个 agent 里做完并集成了 9 个
- 多租户架构终于跑通
- 最后阶段:准备进入 pre-alpha 邀请制测试
现实修正:
我在休假 8 天后又病了 10 天(人生就是会发生这种事)。这让所有进度暂停,时间线整体后移。那个 10 月上线目标?算是泡汤了。
但这就是 solo 开发的好处:节奏你自己掌控。没人在你没准备好时催你发。把它做对,比把它做快更重要。
这篇文章就是一个“把它做对”的瞬间,它最终演变成了一场 6 小时调试马拉松。因为当我刚从生病中恢复、准备重新提速时,部署平台给了我一记反向助攻。
当 AWS 挂掉时,工程师会变得很有创造力(然后偶尔后悔)
代码训练营不会教你的一件事:有时候你的部署平台会“凭空消失”。不是你做错了,而是 AWS 出现大范围故障,全球范围影响了 Vercel 部署。
这就是我周一早上的开局。生产站点挂了,Vercel 一片报错,我脑子里只有一个念头:“得立刻上备份。”
Cloudflare Pages 登场。口碑不错,CDN 强,自动部署,配置看起来很简单。还能出什么问题?
旁白:所有问题都会出。真的所有。
那次看起来“过于顺利”的切换
迁移到 Cloudflare Pages 的过程顺得离谱。连上 GitHub 仓库、在控制台填环境变量、推 main。3 分钟后:部署完成。
我心想:“这也太简单了吧。”
然后我打开生产站点。
```
Mixed Content: The page at 'https://my-site.com/...' was loaded over HTTPS,
but requested an insecure resource 'http://stratum-api.us-central1.run.app/...'
```
那种“刚庆祝完就发现庆祝早了”的坠落感?对,就是那个。
问题:HTTPS 页面里发出了 HTTP 请求
我的 React 应用在 HTTPS 页面中调用后端 API 时,仍在发 HTTP。浏览器理所当然把它拦下(安全风险),于是 Mixed Content 错误出现,每一条 API 请求都失败。
“等等,”我对自己说,“我代码里有 ensureHttpsInProduction() 啊!它不是会自动把 HTTP 变 HTTPS 吗?”
我去看线上 bundle。函数在,逻辑对,控制台也能看到转换动作。那为什么 HTTP 请求还是漏出去了?
第一轮排查:环境变量狩猎
会不会是 Cloudflare 的环境变量根本没生效?
```bash
# Checked Cloudflare Dashboard
VITE_API_URL=https://stratum-api.us-central1.run.app ✓
VITE_SUPABASE_URL=https://your-project.supabase.co ✓
```
全是 HTTPS,全都正确。
我触发重建,等待,部署,刷新站点。
同样的错误。依然是 HTTP 请求。
第二轮排查:大规模 Reimport
会不会是部分文件没用集中化的 API_BASE_URL?
我接下来花了 1 小时,把 24 个文件改成从 @/lib/api 引入,而不是直接使用 import.meta.env.VITE_API_URL。所有发 API 请求的组件都改了一遍。
```typescript
// Before
const response = await fetch(`${import.meta.env.VITE_API_URL}/api/v1/...`);
// After
import \{ API_BASE_URL \} from '@/lib/api';
const response = await fetch(`${API_BASE_URL}/api/v1/...`);
```
推送,部署,等待。
还是坏的。
到这一步,我已经开始反思人生选择。
反转剧情:Git 里缺了关键文件
但还有更糟的。
在继续查 HTTPS 强制逻辑为什么不生效时,我发现一件很恐怖的事:包含 ensureHttpsInProduction() 的 api.ts,竟然根本不在 Git 仓库里。
authService.ts 也不在。csvSanitizer.ts 也不在。三个关键前端文件,全都“蒸发”。
怎么会这样?.gitignore 里有这一段:
```
# Python stuff
lib/
build/
dist/
```
看起来对 Python 很合理,对吧?问题是我的前端工具文件放在 apps/web/src/lib/。这个过于宽泛的 lib/ 规则把整个前端 lib 目录也一起忽略了。
这意味着:
1. Cloudflare 是从仓库构建(缺这些文件)
2. 我本地开发有这些文件(本地看起来正常)
3. 我完全不知道它们没被追踪
修复方式:
```diff
# .gitignore - Before
-lib/
# .gitignore - After
+apps/api/lib/ # Python-specific
+!apps/web/src/lib/ # Explicitly include frontend lib
```
把缺失文件补上、提交、推送。现在 Cloudflare 总该拿到 HTTPS 强制代码了。
结果……HTTP 错误还在。
第三轮排查:构建时校验
到这个阶段我已经开始怀疑宇宙常数。于是我想:“行,那就别让这种问题再有机会进入生产。”
我写了一个 Vite 插件:只要在生产环境检测到 HTTP URL,直接让构建失败。
```typescript
function validateProductionUrls(mode: string) \{
if (mode !== 'production') return;
const apiUrl = process.env.VITE_API_URL || '';
if (apiUrl && apiUrl.trim().startsWith('http://')) {
if (!apiUrl.includes('localhost') && !apiUrl.includes('127.0.0.1')) {
throw new Error(
`❌ HTTPS ENFORCEMENT FAILED
Environment Variable: VITE_API_URL
Current Value: ${apiUrl\}
This will cause Mixed Content errors in production!`
);
}
}
}
```
是不是很聪明?这样就不可能再带着 HTTP URL 部署了。
再次部署。构建通过(因为环境变量确实是 HTTPS)。页面打开。
还是那个该死的错误。
顿悟时刻:本地文件也被部署了
那天晚上很晚的时候,我突然想到一个可能。
我再次检查线上 JavaScript bundle,这次盯得非常仔细。里面的 URL 是:
```javascript
"http://stratum-api.us-central1.run.app"
```
但 Cloudflare 环境变量明明是 HTTPS。那这个 HTTP 到底从哪来的?
然后我意识到:本地 .env.production 文件。
```bash
# apps/web/.env.production (LOCAL FILE)
VITE_API_URL=http://stratum-api.us-central1.run.app
```
Cloudflare Pages 在部署时用了我的本地环境文件,而不是 dashboard 里的变量!
我去看 .cloudflare-pages-ignore:
```
# Environment files
.env
.env.local
.env.development
.env.test
# .env.production ← MISSING!
```
我当场捂脸。
修复:只改一行
```diff
# apps/web/.cloudflare-pages-ignore
.env
.env.local
.env.development
.env.test
+.env.production
```
部署,等待。
这次报了不同错误!有进展!
```
Access to fetch at 'https://stratum-api.us-central1.run.app/...'
from origin 'https://preview-xyz.stratum-marketing-suite.pages.dev'
has been blocked by CORS policy
```
CORS 错误!美丽的 CORS 错误!这意味着 HTTPS 已经生效了!
但还没完:缓存阴谋
修了 CORS,再次部署,然后打开自定义域名。
又是 HTTP 错误。
什么?!
原来 Cloudflare CDN 在激进缓存旧 bundle。新版(HTTPS)在 preview URL 上已经是正确的,但自定义域名还在回旧缓存内容(里面是 HTTP URL)。
Cloudflare 清缓存需要:
1. 找到正确的 zone 设置(不在 Pages 面板里)
2. 穿过域名设置层层菜单(不直观)
3. 手动 purge cache(而且几乎每次部署都要)
在连着几小时排查 HTTP/HTTPS 后,我做了决定。
回归 Vercel:有时候“无聊”就是最优解
AWS 恢复了,Vercel 也正常了。
我把所有东西迁回 Vercel。原因:
1. 自动缓存失效 - 不用手工 purge
2. 环境变量处理更直觉 - 你设置什么就得到什么
3. 调试路径更短 - 基础设施变量更少
4. 久经验证 - 我知道它有哪些坑
Vercel 部署只用 3 分钟。没有 HTTP 错误,没有缓存幻觉,直接可用。
我学到的(以痛换来的)
1. 在部署平台里一定要忽略 .env.production
```
# .vercelignore
# .cloudflare-pages-ignore
# .netlify-ignore
.env
.env.local
.env.development
.env.test
.env.production ← DON'T FORGET THIS
```
2. Monorepo 里宽泛 .gitignore 规则很危险
```diff
# ❌ Bad - Ignores frontend AND backend lib folders
-lib/
-build/
-dist/
# ✅ Good - Specific to each context
+apps/api/lib/ # Python-specific
+apps/api/build/
+apps/api/dist/
+apps/web/dist/ # Vite output only
```
每次都要问自己:“这个规则会不会误伤关键文件?”
在同时有 Python + TypeScript 的 monorepo 里,为一个生态写的宽泛规则,很容易误杀另一个生态里的关键文件。
3. 构建时校验仍然值得保留
即使它没抓到本地文件优先级这个问题,构建时校验仍能防住未来配置错误:
```typescript
// vite.config.ts
export default defineConfig(({ mode }) => {
validateProductionUrls(mode);
return {
// ... config
};
});
```
4. 多层防线真的有效
我们最终架构有三层:
- 构建时:检测到 HTTP URL 直接失败
- 运行时:页面为 HTTPS 时自动把 HTTP 转 HTTPS
- 部署层:排除本地 .env 文件
5. Preview URL 是你的朋友
先测 preview URL。如果 preview 正常但自定义域名异常,通常就是缓存问题。
6. 了解平台“怪癖”
- Vercel:简单、自动失效缓存、环境变量“基本照你预期工作”
- Cloudflare Pages:CDN 很强,但清缓存更手工,配置路径也更复杂
最终救命代码
这是最终版 ensureHttpsInProduction():
```typescript
function ensureHttpsInProduction(url: string): string {
// Only convert in browser context when site is loaded over HTTPS
if (typeof window !== 'undefined' && window.location.protocol === 'https:') {
// Don't convert localhost/127.0.0.1 URLs (local development)
if (url.startsWith('http://') &&
!url.includes('localhost') &&
!url.includes('127.0.0.1')) {
console.warn('[API] Converting HTTP to HTTPS:', url);
return url.replace('http://', 'https://');
}
}
return url;
}
```
以及 vite.config.ts 里的构建时校验:
```typescript
function validateProductionUrls(mode: string) {
if (mode !== 'production') return;
const apiUrl = process.env.VITE_API_URL || '';
// Check for HTTP (should be HTTPS)
if (apiUrl && apiUrl.trim().startsWith('http://')) {
if (!apiUrl.includes('localhost') && !apiUrl.includes('127.0.0.1')) {
throw new Error(`
❌ HTTPS ENFORCEMENT FAILED
Environment Variable: VITE_API_URL
Current Value: ${apiUrl}
Mixed Content Error Prevention:
Browsers block HTTP requests from HTTPS pages.
Fix: Update environment variables to use HTTPS URLs.
`);
}
}
}
```
真正的教训:调试就是侦探工作
这不是编码问题,而是一场配置考古。
真正 bug 有四个:
1. ✅ 过宽的 .gitignore 误忽略前端关键文件
2. ✅ .cloudflare-pages-ignore 里漏了 .env.production
3. ✅ CDN 激进缓存掩盖了修复结果
4. ✅ 对环境变量优先级的错误假设
技术修复是一行 .ignore。
调试过程?6 小时、14 次部署、以及过量咖啡。
值得吗?
绝对值得。我的收获:
1. 更深理解 Mixed Content 安全策略
2. 构建时校验,防止以后再踩
3. 多层 HTTPS 防线,平台无关
4. 真实体会 到 Vercel 的简洁价值
最重要的是:我又多了一篇很好的调试故事可以分享。 :P
Git Log 会说话
```bash
2033b9a fix: add .env.production to .vercelignore
3555390 docs: migrate deployment documentation from Cloudflare Pages to Vercel
a4edb09 chore: force clean Cloudflare Pages rebuild
590c271 fix: enhance ensureHttpsInProduction logging
15d69c6 chore: force rebuild of UserProfile bundle
a1c345f fix: add .env.production to cloudflare-pages-ignore ← THE .ENV FIX
635d80d docs: update documentation for Cloudflare Pages migration
edfa611 feat: add build-time HTTPS enforcement
802c1e9 fix: enforce HTTPS for all API calls across frontend
26f6b87 refactor: comprehensive .gitignore audit and cleanup
3758a9c fix: unignore frontend lib directory and add missing files ← THE GITIGNORE FIX
```
每个 commit 都代表一次假设、一次验证、一次新认知。放在旧世界里这大概是一小时起步的节奏;有 Claude Code 后,至少节奏被加快了很多。
给也在和 Mixed Content 打仗的工程师
如果你正因为同类问题读到这里,这是你的检查清单:
1. 检查环境变量:
```bash
# Print actual values being used
console.log('API URL:', import.meta.env.VITE_API_URL);
```
2. 检查线上 bundle:
```bash
# Download and search the JavaScript bundle
curl https://your-site.com/assets/index-ABC123.js | grep "http://"
```
3. 检查 ignore 文件:
```bash
# Make sure .env.production is excluded
cat .vercelignore
cat .cloudflare-pages-ignore
cat .netlify-ignore
```
4. 检查 .gitignore(Monorepo 尤其要看):
```bash
# Make sure critical files aren't being ignored
git ls-files apps/web/src/lib/ # Should show api.ts, etc.
# If empty, check for broad patterns
grep "^lib/" .gitignore # ❌ Too broad
grep "^apps/api/lib/" .gitignore # ✅ Specific
```
5. 检查缓存:
```bash
# Test on preview URL first
# If preview works but production doesn't = cache issue
```
6. 加构建时校验:
```typescript
// Prevent it from ever happening again
if (mode === 'production' && url.startsWith('http://')) {
throw new Error('HTTPS required in production!');
}
```
我希望自己当时就有的迁移清单
切部署平台时:
- [ ] 把旧平台所有环境变量完整列出来
- [ ] 先在新平台配置环境变量,再做迁移
- [ ] 把所有 .env 文件加入 .ignore(包括 .env.production)
- [ ] 确认 .gitignore 没误伤关键文件(跑 git ls-files 验证)
- [ ] 先在 preview URL 测试,再绑自定义域名
- [ ] 检查线上 bundle 是否残留 HTTP URL
- [ ] 若后端独立部署,确认 CORS 设置
- [ ] 记录平台特有“怪癖”
仍在写代码,仍在学习,仍会偶尔把东西搞坏
为一行修复调了 6 小时。这就是软件工程。
我们写出的代码当然重要,但让我们成为更好工程师的,往往是这些调试能力。
下次部署平台再出问题(一定还会有下次),我已经准备好了:
- ✅ 平台无关的 HTTPS 强制机制
- ✅ 构建时校验
- ✅ 更清晰的环境变量优先级认知
- ✅ 备份部署策略
也希望这篇文章能替别人省掉那 6 小时中的一部分。 :)
你也有过那种“只改一行但找了超久”的调试经历吗?欢迎分享你的侦探故事——痛苦这种事,大家分担会轻一点。
致敬,
Chandler





