开发日志 | 用Cloudflare Workers制作免费的自定义错误页

因为本网站运行在一个个人服务器上,所以每当我对服务器或网络设备进行维护时,都有可能影响到网站的可访问性。一旦网站无法访问,用户就只能看到官方默认的错误页。虽然那个页面很经典,但它有几个问题:

  1. 它不是很好看。
  2. 出现问题后用户无法向我联系或报告,导致问题无法及时被解决。
  3. 用户可能仅仅是输错了域名,却误以为服务器出现问题。(如未输入域名前缀)

虽然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的代码逻辑非常简洁,主要包含几个部分:

  1. 正常转发:async fetch(request)。
  2. 错误拦截:判断 response.status >= 500。
  3. 异常拦截:如果服务器彻底掉线,握手失败,直接默认为522错误。
  4. 测试后门:为了方便在不关停服务器的情况下调试,我加入了 ?test=1 的判断逻辑。
  5. 辅助函数:存储错误码字典,并将错误码和文字信息传递进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

部署注意事项

  1. 在DNS设置中,域名必须开启橙云模式,让Cloudflare代理所有流量,Worker才能生效并拦截。
  2. 在Worker路由配置中,必须使用两条路由分别覆盖子域名(*.example.com/*)和主域名(example.com/*)。
  3. 设置路由时,在请求限制失败模式中选择失败时自动打开,这样在Worker代码有Bug或者额度用完时,Cloudflare会直接绕过Worker访问源站。

最终效果

现在,当我的服务器无法访问时,将会显示我的自定义错误页面。整个过程的体验流畅,内容富有特色,更重要的是这一切的成本是0。

你也可以通过下面的链接来试试自定义错误页面的效果:

https://error.admiralspee.site/(模拟源服务器连接超时)

https://www.admiralspee.site/?test=1(模拟错误503)

Add a Comment

Your email address will not be published. Required fields are marked *