一次移动端随机背景图加载失败的排查:从 Cloudflare WAF 到 type=mobile

发布于 19 天前 0 次阅读 预计阅读时间: 13 分钟 2968 字


AI 摘要

电脑端随机背景图一切正常,手机端却屡屡加载失败?从Cloudflare WAF到防盗链、WebP兼容性,作者经历了一场层层剥茧的排查之旅。最终,元凶竟然藏在前端自动篡改的一个微小参数里!这场看似扑朔迷离的Debug背后,究竟隐藏了怎样意想不到的真相?点击阅读,带你避开那些容易忽视的排查误区。

最近在折腾博客图床和随机背景图时,遇到了一个非常绕的问题:

电脑端随机背景正常,手机端却始终加载不出来。

一开始怀疑过 Cloudflare WAF、防盗链、缓存、CORS,甚至一度怀疑是 WebP 格式兼容问题。

结果一路排查下来,真正的原因却藏在一个不起眼的参数里:

type=mobile

这里记录一下完整的排查过程,也顺便总结一下这次踩到的坑。


我的环境

博客整体结构大概是:

博客 www.example.com → CloudFlare-ImgBed img.example.com → Cloudflare R2

图片实际存储在 R2 中,对外统一通过 CloudFlare-ImgBed 提供。

背景图片目录类似:

blog/site/backgrounds/

桌面端随机背景使用:

https://img.example.com/random?type=img&dir=blog/site/backgrounds&orientation=landscape

移动端使用:

https://img.example.com/random?type=img&dir=blog/site/backgrounds&orientation=portrait

这里的逻辑原本很简单:

orientation=landscape 负责随机横图,orientation=portrait 负责随机竖图。


问题现象

电脑访问博客时,随机背景图一切正常。

但是换成手机访问,背景图始终加载失败。

奇怪的是,如果直接在手机浏览器里打开随机图 API:

https://img.example.com/random?type=img&dir=blog/site/backgrounds&orientation=portrait

接口本身又可以正常访问。

于是第一反应自然变成了:

是不是 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

后来进一步检查手机端实际发出的请求,终于发现了真正的异常。

后台配置明明是:

https://img.example.com/random?type=img&dir=blog/site/backgrounds&orientation=portrait

但是博客主题识别到手机端以后,会对 URL 参数进行处理。

原本的:

type=img

被自动修改成了:

type=mobile

于是手机端真正发出的请求其实变成了:

https://img.example.com/random?type=mobile&dir=blog/site/backgrounds&orientation=portrait

问题就出在这里。


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 控制横图和竖图。

最终配置变成:

桌面端

https://img.example.com/random?type=img&dir=blog/site/backgrounds&orientation=landscape

移动端

https://img.example.com/random?type=img&dir=blog/site/backgrounds&orientation=portrait

这样两个参数的职责就非常清楚了。

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,往往就藏在这中间。