Skip to content
··阅读时间5分钟

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

继续阅读

我的旅程
联系
语言
偏好设置