最近在折腾博客图床和随机背景图时,遇到了一个非常绕的问题:
电脑端随机背景正常,手机端却始终加载不出来。
一开始怀疑过 Cloudflare WAF、防盗链、缓存、CORS,甚至一度怀疑是 WebP 格式兼容问题。
结果一路排查下来,真正的原因却藏在一个不起眼的参数里:
type=mobile
这里记录一下完整的排查过程,也顺便总结一下这次踩到的坑。
我的环境
博客整体结构大概是:
博客 www.example.com → CloudFlare-ImgBed img.example.com → Cloudflare R2
图片实际存储在 R2 中,对外统一通过 CloudFlare-ImgBed 提供。
背景图片目录类似:
blog/site/backgrounds/
桌面端随机背景使用:
移动端使用:
这里的逻辑原本很简单:
orientation=landscape 负责随机横图,orientation=portrait 负责随机竖图。
问题现象
电脑访问博客时,随机背景图一切正常。
但是换成手机访问,背景图始终加载失败。
奇怪的是,如果直接在手机浏览器里打开随机图 API:
接口本身又可以正常访问。
于是第一反应自然变成了:
是不是 Cloudflare 把手机端的图片请求拦掉了?
第一轮排查:Cloudflare WAF
当时图床配置了两套防盗链规则。
普通图片走 /file/*,随机图片接口则单独保护 /random。
随机图规则最初大概按照这样的逻辑判断:
当请求 Host 为 img.example.com、Path 为 /random,并且 Sec-Fetch-Site 为 cross-site,同时 Sec-Fetch-Mode 不是 navigate 时进行拦截。
设计目的其实很简单:
| 请求方式 | 预期结果 |
|---|---|
| 自己博客调用 | ✅ 允许 |
| 浏览器直接打开随机图片 | ✅ 允许 |
| 其他网站直接盗用随机 API | ❌ 拦截 |
检查 Cloudflare Security Events 后,也确实发现过 /random 请求被 Custom Rules 拦截的记录。
所以当时 WAF 确实存在误伤。
随后围绕 Referer、Sec-Fetch-Site 和 Sec-Fetch-Mode 对规则做了几次调整。
调整之后,随机图 URL 已经能够正常直接访问。
但是问题仍然存在:
手机打开博客,随机背景还是不显示。
直接关闭 WAF 测试
为了彻底排除防盗链干扰,后来干脆临时关闭了图床防盗链以及随机图 API 防盗链。
结果手机端依然无法加载随机背景。
到这里基本可以确认:
Cloudflare WAF 并不是最终原因。
虽然排查过程中它确实曾经拦截过部分请求,但那只是另外一个问题,并不是导致移动端背景始终加载失败的根因。
第二个怀疑:WebP 兼容问题
我的博客图片基本都使用 WebP。
于是下一个怀疑目标自然变成了图片格式。
会不会 /random?type=img 返回 WebP 时,在 MIME 类型或者手机浏览器兼容性方面出了什么问题?
为了验证,我把随机背景图临时换成了 JPEG。
测试过程中,JPEG 一度可以正常显示。
这时候特别容易得出一个结论:
难道真的是 WebP 导致手机端无法加载?
但后来证明,这同样是一个误判。
真正的问题修复以后,我重新把图片换回 .webp,移动端依然能够正常加载。
最终结果是:
| 图片格式 | 桌面端 | 移动端 |
|---|---|---|
| JPEG | ✅ | ✅ |
| WebP | ✅ | ✅ |
所以 WebP 并不是根因。
一个非常关键的测试
后来做了一个非常简单,但非常重要的排除测试。
不使用随机 API,直接在博客主题中填写一张固定图片:
https://img.example.com/file/blog/site/backgrounds/example.webp
结果手机端正常显示。
这一下,问题范围瞬间缩小了。
固定 /file/ 图片能够正常加载,而 /random 随机接口无法正常加载。
这意味着图片文件本身、R2、图床文件读取以及 WebP 格式其实都没有问题。
问题一定出在:
/random这条随机图片链路里。
真正的原因:手机端把 type=img 改成了 type=mobile
后来进一步检查手机端实际发出的请求,终于发现了真正的异常。
后台配置明明是:
但是博客主题识别到手机端以后,会对 URL 参数进行处理。
原本的:
type=img
被自动修改成了:
type=mobile
于是手机端真正发出的请求其实变成了:
问题就出在这里。
type=img 和 type=mobile 完全不是一回事
在 CloudFlare-ImgBed 的随机图接口中,type=img 的意思是:
直接返回图片内容。
请求过程可以理解成:
/random?type=img
↓
随机选择图片
↓
直接返回图片
↓
浏览器显示
所以电脑端一直能够正常工作。
但 type=mobile 对这个 API 来说,并不是“返回手机图片”。
实际请求以后,它返回的是类似这样的 JSON:
{
"url": "/file/blog/site/backgrounds/example.webp"
}
于是手机端真实发生的事情变成了:
/random?type=mobile
↓
返回 JSON
↓
博客前端仍然把响应当成图片
↓
加载失败
到这里,之前所有看起来很奇怪的现象就全部解释得通了。
为什么直接打开随机 API 又正常?
因为之前手动测试的时候,我一直打开的是:
/random?type=img
这个接口本身当然没有问题。
但是博客移动端真正请求的却是:
/random?type=mobile
也就是说:
后台配置的 URL,不一定等于浏览器最终实际请求的 URL。
这也是整个排查过程中最容易被忽略的地方。
一直手动测试 /random?type=img,实际上根本没有复现博客移动端真实发生的请求。
最终解决办法
找到移动端修改 URL 参数的逻辑以后,解决办法反而非常简单:
取消主题把 type=img 自动修改为 type=mobile 的行为。
让手机端继续使用:
type=img
然后只通过 orientation 控制横图和竖图。
最终配置变成:
桌面端
移动端
这样两个参数的职责就非常清楚了。
type=img 负责告诉随机 API:
我要直接获取图片。
而 orientation=landscape 和 orientation=portrait 负责告诉随机 API:
我要横图还是竖图。
设备类型根本没有必要通过修改 type 参数来处理。
修复结果
修改完成以后重新测试:
| 测试项目 | 结果 |
|---|---|
| 电脑 + WebP | ✅ 正常 |
| 手机 + WebP | ✅ 正常 |
| 电脑 + JPEG | ✅ 正常 |
| 手机 + JPEG | ✅ 正常 |
直接访问 /random | ✅ 正常 |
| 随机横图 | ✅ 正常 |
| 随机竖图 | ✅ 正常 |
这也最终证明了,WebP 没有问题。
所以背景图库继续使用 WebP 即可,没有必要因为这次故障重新换回 JPEG。
WAF 规则也不是白折腾
虽然 Cloudflare WAF 最终不是这次故障的根因,但排查过程中也顺便把图床防盗链重新理清楚了。
例如普通图片 /file/*,可以使用 Referer 做一层简单的防盗链。
大致思路是:
请求 Host 为 img.example.com,Path 以 /file/ 开头,并且存在 Referer;如果 Referer 既不是 example.com,也不是它允许的子域名,则进行拦截。
最终希望达到这样的效果:
| 场景 | 结果 |
|---|---|
| 自己博客引用图片 | ✅ |
| 自己其他子域引用 | ✅ |
| 浏览器直接打开图片 | ✅ |
| 普通第三方网站盗链 | ❌ |
随机图片接口 /random 也可以单独保护。
不过经过这次排查,我反而觉得:
个人博客的防盗链规则没有必要写得过于激进。
尤其是 Referer、Sec-Fetch-Site 和 Sec-Fetch-Mode,在不同浏览器、WebView 和移动端环境中的表现并不一定完全一致。
如果只是个人博客图床,而不是严格的私有文件访问系统,做基础防盗链通常就已经足够。
规则写得太复杂,很容易出现一种尴尬情况:
别人还没被拦,自己先被拦了。
这次排查踩到的几个坑
1. Cloudflare 出现 Block,不代表它就是唯一问题
Security Events 中确实出现过 /random → Block。
这说明 WAF 当时存在误伤。
但是关闭 WAF 以后,移动端背景依然无法加载。
所以:
发现一个问题,不等于找到了最终根因。
排障过程中很容易被第一个异常现象带偏。
2. 换成 JPEG 后能显示,不代表 WebP 就有问题
排查过程中曾经临时换过 JPEG,而某次测试又刚好能够正常显示。
这种情况下很容易把问题归结为 WebP 兼容性。
但真正解决 type=mobile 的问题以后,再换回 WebP 一样能够正常加载。
所以调试时最好一次只改变一个变量。
如果 WAF、缓存、图片格式、API 参数和主题代码一起改,即使最后恢复正常,也很难知道真正是哪一步起了作用。
3. 后台配置的 URL,不一定就是浏览器真正请求的 URL
这是这次排查中最重要的一条经验。
后台明明填写的是 type=img,浏览器最终实际发出的请求却可能已经变成了 type=mobile。
所以遇到前端资源加载异常时,不能只看:
我后台填写了什么?
更应该确认:
浏览器最后到底请求了什么?
可以通过浏览器开发者工具、服务器日志或者 Cloudflare 流量记录查看实际请求。
修复后的请求链路
现在桌面端的逻辑变成:
博客主题
↓
/random?type=img&orientation=landscape
↓
CloudFlare-ImgBed
↓
随机选择横图
↓
Cloudflare R2
↓
返回 WebP
移动端则是:
博客主题
↓
/random?type=img&orientation=portrait
↓
CloudFlare-ImgBed
↓
随机选择竖图
↓
Cloudflare R2
↓
返回 WebP
设备之间的差异只交给 orientation 处理。
而 type 始终保持为 img。
这样主题和图床 API 两边的参数含义就不会再互相冲突。
总结
这次移动端随机背景图片加载失败,前后怀疑过 Cloudflare WAF、R2、防盗链、缓存、CORS、JPEG、WebP 以及手机浏览器兼容性。
但最终真正的问题只有一个:
博客主题在手机端把 type=img 自动修改成了 type=mobile。
这导致 CloudFlare-ImgBed 的 /random 接口不再直接返回图片,而是返回类似:
{
"url": "/file/xxx.webp"
}
前端又继续把这段 JSON 当成图片资源使用,于是最终加载失败。
解决办法其实很简单:
保证移动端依然请求 type=img,然后使用 orientation=portrait 控制移动端竖图即可。
这次排查下来,最大的经验大概就是一句话:
不要只看“我配置了什么”,一定要看客户端最后“实际请求了什么”。
因为后台写着 type=img,并不意味着浏览器最后请求的也一定还是 type=img。
真正的 Bug,往往就藏在这中间。

Comments NOTHING