AI 摘要

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

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

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

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

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

type=mobile

这里记录一下完整的排查过程。


我的环境

博客大概是这样的结构:

博客
https://www.example.com

        ↓

CloudFlare-ImgBed
https://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/*

另一条用于保护随机图 API:

/random

随机图规则最初大概是按照下面的逻辑配置:

Host = img.example.com

Path = /random

Sec-Fetch-Site = cross-site

并且

Sec-Fetch-Mode != navigate

→ Block

目的很简单:

自己博客调用
✅

直接打开随机图片
✅

其他网站直接盗用随机 API
❌

Security Events 里确实出现过 Block

在 Cloudflare 的安全事件中,根据 Ray ID 可以查到类似请求:

Service: Custom Rules
Action: Block
Host: img.example.com
Path: /random

所以当时确实存在 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

AND

Path starts with /file/

AND

Referer != ""

AND

Referer 不是:

https://example.com/*
https://*.example.com/*

→ Block

效果大概是:

自己博客引用
✅

自己的其他子域引用
✅

直接打开图片 URL
✅

普通第三方网站盗链
❌

随机图:

/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
    ↓
随机选择横图
    ↓
R2
    ↓
返回 WebP

移动端:

博客主题
    ↓
/random?type=img
&orientation=portrait
    ↓
CloudFlare-ImgBed
    ↓
随机选择竖图
    ↓
R2
    ↓
返回 WebP

设备差异只交给:

orientation

处理。

而:

type

始终保持:

img

这样两个系统之间的参数逻辑就不会互相冲突。


总结

这次移动端随机背景图片加载失败,前后怀疑过:

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