Nginx 自定义错误页面:提升用户体验

配置有用的 Nginx 自定义错误页面,用于 404、403 和 50x 响应,同时不隐藏真正的故障。

Nginx 自定义错误页面:提升用户体验

Nginx 自定义错误页面将原始故障转化为清晰的下一步操作。它们不会修复损坏的链接、缺失的文件或崩溃的上游应用。它们做的事情虽小但仍有价值:用通俗的语言告诉访问者发生了什么,并避免让他们感觉被赶出了你的网站。

这在普通错误发生时很重要。有人点击了旧文档链接,得到 404。私有文件路径返回 403。你的应用在部署期间重启,Nginx 短暂看到 502。如果没有自定义页面,用户可能会看到默认的服务器响应,显得突兀或技术化。而有了好的静态页面,他们就知道是搜索、返回、重试还是等待。

最好的错误页面是枯燥的操作工具。它们静态、快速、可访问且诚实。它们不会向监控隐藏故障,也不会向用户暴露内部细节。

从用户实际看到的错误开始

你不需要为每个 HTTP 状态码设计页面。从常见的开始。

404 Not Found 是第一个需要自定义的页面。当请求的 URL 与返回内容的文件、路由或 Nginx 位置不匹配时出现。旧链接、重命名后的文章、已删除的文档页面以及手动输入的 URL 都会导致这个错误。

一个有用的 404 页面会说:“我们找不到那个页面。”然后提供返回首页、文档索引、产品区域或搜索页面的路径。不要责怪用户。URL 可能在他们点击之前就已经错了很久。

403 Forbidden 则不同。Nginx 理解了请求但不会提供它。原因包括文件权限、访问规则、禁用的目录列表、IP 允许/拒绝规则或认证要求。403 页面应保持冷静简短。如果资源是私有的,就说明这一点。如果用户可能需要访问,引导他们到正确的登录或支持路径。

对于基于应用的网站,小心处理 50x 错误:

  • 500 Internal Server Error 通常意味着应用在处理请求时失败。
  • 502 Bad Gateway 通常意味着 Nginx 没有从上游服务收到有效响应。
  • 503 Service Unavailable 适用于维护、过载或故意不可用的服务。
  • 504 Gateway Timeout 意味着 Nginx 等待上游响应时间过长。

一个 SaaS 仪表盘示例:你的应用进程在部署期间重启。几秒钟内,Nginx 无法连接到上游,用户看到 502。自定义页面可以说:“仪表盘暂时不可用。请稍后刷新。”这并不完美,但比默认的网关错误更清晰。

创建静态错误文件

将错误页面放在应用依赖链之外。如果数据库宕机,500 页面仍应加载。如果 Node、Python、Ruby 或 PHP 应用不健康,Nginx 仍应提供静态回退。

一个简单的文件布局可能是:

/var/www/example.com/public/
  index.html
  assets/
/var/www/example.com/errors/
  404.html
  403.html
  50x.html

保持 HTML 轻量。避免第三方脚本、大图片、客户端应用包以及任何调用故障后端的内容。如果 Nginx 可以直接提供,一个小 CSS 文件是可以的。

一个最小的 404.html 可以是:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>页面未找到</title>
</head>
<body>
  <main>
    <h1>页面未找到</h1>
    <p>该页面可能已移动,或链接已过时。</p>
    <p><a href="/">前往首页</a></p>
  </main>
</body>
</html>

这就够了。你可以设置样式以匹配你的网站,但消息和链接比装饰更重要。

使用 error_page 连接页面

在 Nginx 中,error_page 指令将一个或多个状态码映射到一个 URI。一个基本的服务器块如下所示:

server {
    listen 80;
    server_name example.com www.example.com;

    root /var/www/example.com/public;

    error_page 404 /errors/404.html;
    error_page 403 /errors/403.html;
    error_page 500 502 503 504 /errors/50x.html;

    location /errors/ {
        internal;
        root /var/www/example.com;
    }

    location / {
        try_files $uri $uri/ =404;
    }
}

路径解析是容易出错的部分。在这个例子中,/errors/ 下的请求使用 root /var/www/example.com;,所以 /errors/404.html 映射到 /var/www/example.com/errors/404.html

internal 指令意味着外部客户端不能直接以普通 URL 请求 /errors/404.html。Nginx 在处理错误时仍可以在内部提供它。

编辑配置后,测试并重新加载:

sudo nginx -t
sudo systemctl reload nginx

然后测试行为:

curl -I http://example.com/definitely-missing-page
curl -s http://example.com/definitely-missing-page | head

状态码仍应为 404,即使正文是你的友好 HTML。一个常见错误是意外地为缺失页面返回 200 OK,这会使监控和搜索引擎认为缺失的 URL 是一个真实页面。

反向代理后的自定义页面

如果 Nginx 代理到上游应用,应用可能返回自己的错误响应。默认情况下,代理响应通常直接传回客户端。要让 Nginx 拦截上游错误响应并使用你的 error_page 规则,在相关上下文中启用 proxy_intercept_errors

location /app/ {
    proxy_pass http://app_backend;
    proxy_intercept_errors on;

    error_page 502 503 504 /errors/50x.html;
}

Nginx 文档描述 proxy_intercept_errors 适用于状态码大于等于 300 的代理响应,这些响应可以被重定向到 Nginx 进行 error_page 处理。实际上,不要不加思考地到处启用它。

对于浏览器页面,拦截 502 或 503 通常有用。对于 JSON API,可能不合适。API 客户端通常期望结构化的 JSON 错误体,而不是 HTML 页面。你可能需要单独的位置:

location /api/ {
    proxy_pass http://api_backend;
    proxy_intercept_errors off;
}

location /dashboard/ {
    proxy_pass http://app_backend;
    proxy_intercept_errors on;
    error_page 502 503 504 /errors/50x.html;
}

这种分离保持了面向用户的页面友好,同时保留了机器可读的 API 错误。

保留正确的状态码

Nginx 允许你通过 error_page 更改响应码,但只有在有意为之时才这样做。这是有效的语法:

error_page 404 =200 /fallback.html;

对于大多数网站,这将是一个坏主意。缺失页面应保持为 404。搜索引擎、正常运行时间检查、分析和用户都受益于真相。

有一些合法的情况需要更改状态码,例如将某些错误路由到命名位置或返回带有 503 的维护页面。但作为默认规则,保留原始错误状态。

对于维护,你可以明确:

location / {
    return 503;
}

error_page 503 /errors/maintenance.html;

location = /errors/maintenance.html {
    root /var/www/example.com;
    internal;
}

如果你在 Nginx 前面使用 CDN 或负载均衡器,请记住它可能有自己的错误页面行为。决定哪个层负责哪些错误。否则,你可能直接测试 Nginx 时看到一个页面,而 CDN 后面的用户看到另一个。

为人类编写错误页面

内容应快速回答三个问题:

  • 发生了什么?
  • 是暂时的吗?
  • 我接下来可以做什么?

对于 404,有用的下一步是搜索、首页、文档索引,或者如果缺失页面应该存在则联系支持。对于 503,有用的指导是稍后重试或检查状态页面。对于 403,如果合适,指向登录或访问请求说明。

避免堆栈跟踪、上游主机名、文件系统路径、包版本、内部 IP、无解释的请求 ID 以及事件细节。如果支持可以使用请求 ID,那可能有用,但要清晰标记:

<p>如果您联系支持,请包含此请求 ID:<code>$request_id</code></p>

要注入像 $request_id 这样的变量,你需要一个支持它的配置模式。静态 HTML 文件本身不会展开 Nginx 变量。许多团队保持公共错误页面静态,并依赖日志获取请求 ID。

可访问性是有用性的一部分。使用一个清晰的 h1、可读的对比度、普通链接和纯文本。不要将唯一的恢复操作设为小图标或脚本驱动的按钮。

故意测试页面

不要等到用户发现你的错误页面。每次更改后都要测试。

对于 404:

curl -i https://example.com/no-such-page

对于 403,创建一个受控的测试位置或使用私有测试文件。不要为了触发错误而放宽生产权限。

对于 502 或 503,在测试环境中通过将位置指向不可用的上游来测试:

location /broken-upstream-test/ {
    proxy_pass http://127.0.0.1:59999;
    proxy_intercept_errors on;
    error_page 502 503 504 /errors/50x.html;
}

然后请求它并确认状态码和正文:

curl -i https://staging.example.com/broken-upstream-test/

同时查看日志:

sudo tail -f /var/log/nginx/error.log /var/log/nginx/access.log

一个好看的错误页面不应抹去操作信号。当 50x 率上升时,你的警报仍应触发。

Nginx 自定义错误页面是一个小配置任务,但对用户有实际影响。从 404、403 和常见的 50x 错误开始。直接从 Nginx 提供静态文件。保留准确的状态码。仅在 HTML 回退有意义的地方使用 proxy_intercept_errors。然后像测试任何其他生产行为一样测试这些页面。