开发日志 | 用Cloudflare Workers制作免费的自定义错误页
Posted On 2026-01-13
因为本网站运行在一个个人服务器上,所以每当我对服务器或网络设备进行维护时,都有可能影响到网站的可访问性。一旦网站无法访问,用户就只能看到官方默认的错误页。虽然那个页面很经典,但它有几个问题:
- 它不是很好看。
- 出现问题后用户无法向我联系或报告,导致问题无法及时被解决。
- 用户可能仅仅是输错了域名,却误以为服务器出现问题。(如未输入域名前缀)
虽然Cloudflare官方提供了“Custom Pages”功能,但这个需要订阅Business Plan ($200/month)才能解锁。对于个人开发者来说,为了修改一个页面支付这个成本显然不太划算。
不过我发现我可以使用Cloudflare Workers来“拦截”请求,然后将自定义错误页面发送给用户。
- 原理:Worker运行在边缘节点,当它发现源站返回 5xx 错误(或直接连不上)时,不是直接把错误扔给用户,而是由Worker动态生成并返回一段预设好的HTML。
- 成本:Worker每天有 10 万次免费额度,对于个人网站来说绰绰有余。
技术架构
单文件化
这个方法最核心的挑战在于:当服务器挂了的时候,我不能依赖服务器上的任何资源(图片、CSS)。
对于HTML和CSS来说很好解决,只要全部写入Worker脚本中即可。
但是图片资源如果用外部链接的形式,同样会有无法访问的可能。所以我将所有图片资源全部转换为Base 64编码,直接内联写入HTML。
显然这样整个页面就只有一个HTML文件,不依赖任何外部请求,加载速度极快且极其稳定。
动态替换
对于错误页面,最重要的是传达错误代码和信息。我在HTML模板中设置了两个占位符:
- {{ERROR_CODE}}:用于显示错误代码(如503)。
- {{ERROR_DESC}}:用于显示具体的说明。
Worker在拦截到错误响应后,会读取response.status,查表获取对应的文案,然后用正则填充这两个占位符。
开发代码
Worker的代码逻辑非常简洁,主要包含几个部分:
- 正常转发:async fetch(request)。
- 错误拦截:判断 response.status >= 500。
- 异常拦截:如果服务器彻底掉线,握手失败,直接默认为522错误。
- 测试后门:为了方便在不关停服务器的情况下调试,我加入了 ?test=1 的判断逻辑。
- 辅助函数:存储错误码字典,并将错误码和文字信息传递进HTML里。
下面是核心代码片段:
export default {
async fetch(request) {
try {
const url = new URL(request.url);
// 1. 测试后门:?test=1,强制显示 503
if (url.searchParams.get("test") === "1") {
return generateErrorResponse(503, htmlContent);
}
const response = await fetch(request);
// 2. 拦截 5xx 错误
if (response.status >= 500) {
// 传入真实的 response.status
return generateErrorResponse(response.status, htmlContent);
}
return response;
} catch (e) {
// 3. 彻底断网时的兜底 默认给 522
return generateErrorResponse(522, htmlContent);
}
},
};
// 把错误码和错误说明传递进 HTML
function generateErrorResponse(statusCode, htmlTemplate) {
// 定义不同错误码说明
const messages = {
500: "源服务器收到了你的请求,但内部出现了错误或故障导致无法访问。",
502: "Cloudflare暂时无法与源服务器取得联系。",
503: "服务暂时不可用,可能正在维护或访问过载。",
504: "Cloudflare与源服务器的连接超时了。",
520: "未知错误,源服务器返回了一些莫名其妙的数据。",
521: "源服务器不知道为什么拒绝了你的连接。",
522: "Cloudflare与源服务器的连接超时了。",
523: "Cloudflare没有找到源服务器的准确位置。",
525: "源服务器的SSL证书过期了,请等待工程师续费。",
"default": "你似乎遇到了某些罕见的错误,工程师正在加紧排查中。"
};
// 获取对应的文案
const desc = messages[statusCode] || messages["default"];
// 替换代码和文案
let finalHtml = htmlTemplate.replace(/{{ERROR_CODE}}/g, statusCode);
finalHtml = finalHtml.replace(/{{ERROR_DESC}}/g, desc);
return new Response(finalHtml, {
status: 503,
headers: { "content-type": "text/html;charset=UTF-8" },
});
}
const htmlContent = `
// 在此处添加HTML模版。
// 在需要显示错误代码的地方使用{{ERROR_CODE}}。
// 在需要显示错误描述的地方使用{{ERROR_DESC}}。
`;JavaScript部署注意事项
- 在DNS设置中,域名必须开启橙云模式,让Cloudflare代理所有流量,Worker才能生效并拦截。
- 在Worker路由配置中,必须使用两条路由分别覆盖子域名(*.example.com/*)和主域名(example.com/*)。
- 设置路由时,在请求限制失败模式中选择失败时自动打开,这样在Worker代码有Bug或者额度用完时,Cloudflare会直接绕过Worker访问源站。
最终效果
现在,当我的服务器无法访问时,将会显示我的自定义错误页面。整个过程的体验流畅,内容富有特色,更重要的是这一切的成本是0。
你也可以通过下面的链接来试试自定义错误页面的效果:
https://error.admiralspee.site/(模拟源服务器连接超时)
https://www.admiralspee.site/?test=1(模拟错误503)