Linux Nginx 怎么在配置文件中配置不同状态码的错误页面

发布时间:2026/10/4 12:36:38

Linux Nginx 怎么在配置文件中配置不同状态码的错误页面 前言默认情况下Nginx 对 404、502 这类错误会返回它自带的极简页面白底黑字一行404 Not Found加一行版本号。放在生产站点上既和站点风格不搭又把版本信息暴露给了外部。于是大家都会去配error_page。真正让人头疼的是配完之后的各种不生效静态文件 404 改好了但后端接口返回的 404 依然是后端自己的页面error_page完全没参与写了个统一的错误页 URI结果浏览器一直在转圈日志里刷rewrite or internal redirection cycle错误页配好了用curl一测返回的是 200监控从此再也发现不了 404 和 502把错误页放在location /下客户端可以直接访问那个地址搜索引擎把它当成正常内容收录。这些现象背后是三个知识点error_page默认只管 Nginx 自己产生的响应码、内部跳转有循环检测、以及那个能改写状态码的等号写法。本文基于 nginx 1.24RHEL 9 与 Ubuntu 22.04nginx.org 官方仓库包逐条讲清楚最后给一套可以直接复制的完整配置并附上每条验证用的curl命令。一、error_page的语法与作用范围完整语法是error_page code ... [[response]] uri;三部分分别对应三个能力code可以写多个状态码用空格隔开用来改写最终返回给客户端的状态码uri决定错误页从哪里来。作用范围上下文是http、server、location、location中的 if也就是可以分层覆盖——http 层配一个全局兜底某个 location 再单独覆盖。uri的三种形态行为完全不同这是最需要分清的一点uri写法行为客户端看到的状态码以/开头如/404.htmlNginx 内部重定向重新走一遍 location 匹配原错误码除非用改写name命名 location内部跳转到命名 location不经过 URI 匹配原错误码除非用改写带协议头的完整地址非斜杠开头返回 302让客户端自己去取那个地址302后面接 URI如 /404.php内部跳转采用内部请求返回的状态码由内部请求决定200加 URI如200 /empty.gif内部跳转并强制改写为 200200多个状态码可以共用同一个页面这是最常见的写法error_page 500 502 503 504 /50x.html;顺带一提RHEL 和 Debian 系的默认nginx.conf里本来就带了error_page的示例形如error_page 404 /404.html;配一个location /404.html的空块可以把发行版自带的配置文件当参考读一遍再看自己哪里写得不一样。二、为什么配了不生效上游返回的状态码要单独放行这是error_page最反直觉的地方。默认情况下error_page只处理 Nginx 自己生成的状态码比如文件不存在产生的 404、后端连不上产生的 502、连接超时产生的 504。而由上游应用proxy_pass打到 Tomcat、Go、PHP-FPM 等主动返回的 404、403、500Nginx 会原样透传给客户端error_page根本不会介入。要让 Nginx 也接管上游返回的错误码必须显式打开拦截上游错误的开关server { listen 80; server_name www.example.com; location /api/ { proxy_pass http://app_backend; proxy_intercept_errors on; # ✅ 让 error_page 接管上游返回的错误码默认 off } location ~ \.php$ { fastcgi_pass unix:/run/php-fpm/www.sock; fastcgi_intercept_errors on; # ✅ PHP-FPM 场景的对应开关默认 off } }对应的开关按上游类型区分指令名不能混用上游类型拦截开关默认值上下文proxy_passproxy_intercept_errorsoffhttp、server、locationfastcgi_passfastcgi_intercept_errorsoffhttp、server、locationuwsgi_passuwsgi_intercept_errorsoffhttp、server、locationscgi_passscgi_intercept_errorsoffhttp、server、location打开这个开关有两个代价必须知道。第一上游返回的响应体body会被丢弃客户端拿到的是 Nginx 错误页而不是应用精心设计的错误提示——所以要权衡是想统一风格还是想保留应用自己的错误信息。第二它只对大于等于 300的响应码生效2xx 不受影响。还有一个容易混淆的点上游连不上和上游主动返回错误码是两回事。后端进程挂掉时 Nginx 压根拿不到响应这个 502 是 Nginx 自己生成的error_page天然就能接管不需要打开proxy_intercept_errors。所以出现502 的错误页生效了404 的没生效这种情况时不用怀疑配置语法就是拦截开关的问题。三、internal、命名 location 与状态码改写error_page指向的是一个普通 URI 时它同时也变成了一个可以被外部直接访问的地址。也就是说你的/404.html任何人都能直接打开而且返回 200搜索引擎会把它当成一个正常页面收录监控也不会把它算作错误。修法有两种。用internal指令关闭外部访问error_page 404 /404.html; location /404.html { root /usr/share/nginx/html; internal; # ✅ 只允许内部跳转访问外部直接请求返回 404 }internal的上下文是 location。它的效果是外部请求/404.html会得到 404而error_page触发的内部跳转可以正常拿到这个文件。注意location /404.html这里的等号是精确匹配必须和error_page里的 URI 完全一致写成location /404.html虽然也能匹配但精确匹配的优先级更高、语义更清晰。用命名 location它天生不能被外部访问error_page 404 not_found; location not_found { # 命名 location 内部的 URI 不会重新走一遍 location 匹配 return 404 页面不存在\n; }两者的区别值得单独列一下方式可否被外部直接访问是否会重新匹配 location适用场景location /404.htmlinternal否有internal时会静态错误页文件location name否天生不可访问不会交给return、proxy_pass处理接下来是那个会引发循环的坑。error_page触发的内部跳转会重新走 location 匹配如果匹配到的 location 里又有error_page指向同一个地方就会来回跳。Nginx 有循环检测超过内部跳转次数上限后返回 500并在 error_log 里写下[error] ... rewrite or internal redirection cycle while internally redirecting to /404.html典型触发场景是给错误页配了root指向的目录本身不存在或者错误页文件缺失——Nginx 去取/404.html又得到 404于是再次触发error_page 404。另一个场景是错误页 URI 被location /里的规则重写到了别处。针对错误页本身也出错的情况Nginx 提供recursive_error_pages指令http、server、location默认 off来允许连续多次内部跳转recursive_error_pages on; # 允许 error_page 连续跳转多次默认关闭除非确实需要多级兜底比如先试静态页、失败再试后端接口否则不建议打开——它会掩盖循环配置让问题从500 报错变成无限跳转直到超时。最后是等号改写状态码的能力它解决的是另一类需求某些单页应用SPA希望所有未命中的路由都返回 200 交给前端路由处理而不是 404。写成200即可error_page 404 200 /index.html;后面不写状态码时形如error_page 404 /404.php;返回给客户端的状态码取自内部请求的结果——这对于先把请求交给后端接口用接口返回的码作为最终码的场景很有用。但要清楚改写的代价监控和搜索引擎依赖状态码判断资源是否存在一旦改成 200404 就彻底不可见了。如果只是担心搜索引擎收录错误页正确做法是在错误页里加noindex而不是把状态码改成 200。四、实战一套覆盖多种状态码的完整配置下面这套配置按客户端错误和服务端错误分成两组静态错误页统一放/usr/share/nginx/html/errors/同时覆盖了上游返回的错误码。server { listen 80; server_name www.example.com; root /usr/share/nginx/html; index index.html; # ---- 客户端错误404 / 403 用同一个页面风格的模板 ---- error_page 403 /errors/403.html; error_page 404 /errors/404.html; # ---- 服务端错误500 系全部走同一个兜底页 ---- error_page 500 502 503 504 /errors/50x.html; # ---- 上游返回的错误码也交给 error_page 处理 ---- location /api/ { proxy_pass http://app_backend; proxy_intercept_errors on; proxy_set_header Host $host; } # ---- 错误页本体只允许内部访问 ---- location ^~ /errors/ { internal; root /usr/share/nginx/html; } # ---- SPA 场景未命中的路由交给前端处理注意状态码变成 200 ---- location /app/ { try_files $uri $uri/ /app/index.html; } }配套的错误页文件保持轻量不要在错误页里引用外部资源错误状态下它们很可能也取不到sudo mkdir -p /usr/share/nginx/html/errors sudo tee /usr/share/nginx/html/errors/404.html /dev/null EOF !doctype html meta charsetutf-8 title404/title h1页面不存在/h1 p请检查地址是否正确。/p EOF sudo cp /usr/share/nginx/html/errors/404.html /usr/share/nginx/html/errors/403.html sudo cp /usr/share/nginx/html/errors/404.html /usr/share/nginx/html/errors/50x.html # RHEL 系若启用了 SELinux恢复一次上下文 sudo restorecon -Rv /usr/share/nginx/html部署与逐条验证# 1) 语法检查任何 error_page 写法错误都会在这里暴露 sudo nginx -t # 2) 确认 error_page 和 intercept 指令确实被加载 sudo nginx -T | grep -nE error_page|intercept_errors|recursive_error_pages # 3) 平滑重载 sudo nginx -s reload # 4) 逐种状态码验证只看状态码不看内容 for u in /errors/404.html /nonexistent-page /api/not-exist; do code$(curl -sS -o /dev/null -w %{http_code} -H Host: www.example.com http://127.0.0.1$u) printf %-24s - %s\n $u $code done第 4 条命令的预期结果和判读方式如下这台机器上 back_end 是正常运行的/errors/404.html - 404 # internal 生效外部直接访问被拒绝 /nonexistent-page - 404 # 静态 404走自定义错误页 /api/not-exist - 404 # 上游返回的 404 被 proxy_intercept_errors 接管如果/errors/404.html返回的是 200说明internal没生效或者 location 没匹配上如果/api/not-exist返回的是上游自己的页面而不是你的 404 页说明proxy_intercept_errors没打开。想看到具体页面内容时把-o /dev/null去掉加-i可以直接看到响应头。再补一条排查循环跳转的命令出问题时能第一时间定位# 实时跟踪错误日志观察是否有内部重定向循环 sudo tail -f /var/log/nginx/error.log | grep -i cycle常见坑点❌ 上游接口返回的 404 不生效反复检查error_page语法✅proxy_pass/fastcgi_pass场景必须分别打开proxy_intercept_errors/fastcgi_intercept_errors默认都是 off❌ 错误页配了location /404.html { }却没写internal✅ 外部可以直接访问该地址并得到 200搜索引擎会收录监控也看不见必须加internal或改用命名 location❌ 错误页文件路径写错导致取错误页时又得 404来回跳✅ 日志出现rewrite or internal redirection cycle就是这个原因先确认错误页文件真实存在、root指向正确❌ 为了让 SPA 的路由正常无脑写error_page 404 200 /index.html;✅ 状态码改成 200 后 404 在监控和搜索引擎里完全消失要在错误页里加noindex而不是改状态码❌ 在location /api/里配了error_page但错误页的 location 用的是^~前缀又放在后面✅ 内部跳转会重新做 location 匹配前缀匹配与正则匹配的优先级要提前理清实在不确定就用命名 location❌ 打开proxy_intercept_errors on之后发现应用返回的错误详情不见了✅ 该开关会丢弃上游响应体用 Nginx 的错误页替换如果想保留应用信息就不要打开它❌ 只测了 404没测 502/504上线后才发现后端挂掉时页面是默认的✅ 后端错误页要么停掉后端实测要么临时把proxy_pass指向一个不存在的端口来验证 502❌ 错误页里引用了站点的 CSS/JS 和图片✅ 错误状态下这些静态资源请求也可能失败错误页应当是自包含的纯 HTML样式内联总结需求写法关键点不同状态码用不同页面error_page 404 /errors/404.html;多个码可写在一行空格分隔500 系共用一个兜底页error_page 500 502 503 504 /errors/50x.html;502/504 由 Nginx 生成天然可捕获接管上游返回的错误码proxy_intercept_errors on;默认 off会丢弃上游响应体错误页不可被外部访问internal;或location name命名 location 天生不可外部访问改写返回给客户端的状态码error_page 404 200 /empty.gif;后不写码则取内部请求的状态码避免跳转循环检查错误页文件与 location 匹配recursive_error_pages默认 off不建议随意打开验证nginx -T看加载内容curl -w %{http_code}看实际码状态码和页面内容都要各测一遍配error_page的关键不在语法而在两个隐含前提上游返回的错误码默认不归 Nginx 管以及错误页本身在 Nginx 眼里就是一次普通的内部请求——它会重新匹配 location、可能是 200、也可能再次出错。把这两个前提想清楚再按静态错误 → 上游错误 → 状态码改写的顺序逐个验证错误页配置就不会再出现看起来写了、实际没生效的情况。
延伸阅读

更多相关文章

2026/10/4 12:36:38

原生Java实现数值分析与机器学习算法:源码全解析

简介:在Java后端开发中,数值计算与机器学习往往被视为Python的专属领域。然而,理解算法底层原理对工程实践至关重要。无论是数值积分中的梯形公式与辛普森公式,还是常微分方程求解中的四阶龙格-库塔法,这些数值分析方法…

2026/10/4 12:36:38

C++中的friend函数详细解析

为什么要使用友元函数在实现类之间数据共享时,减少系统开销,提高效率。如果类A中的函数要访问类B中的成员(例如:智能指针类的实现),那么类A中该函数要是类B的友元函数。具体来说:为了使其他类的…

2026/10/4 12:31:38

多编码代理模型切换混乱?用菜单栏统一编排二十多个代理

1. 从二十多个编码代理的切换混乱说起如果你同时用过三款以上的编码代理工具,大概率经历过这种场景:左边窗口跑着一个负责补全的,右边终端挂着一个负责重构的,浏览器里还开着第三个在生成测试用例。每个工具都有自己的模型选择入口…

2026/10/4 20:51:59

IDE插件机制深度解析:从activationEvents到CLI协同

1. “plugins”不是功能模块,而是现代开发工具的神经末梢你点开 Cursor、VS Code、JetBrains IDE 的插件市场时,看到的“Plugins”三个字母,绝不是菜单栏里一个可有可无的二级入口。它本质上是一套运行时可加载、沙盒化隔离、声明式注册、事件…

2026/10/4 20:51:59

插件体系全解析:从plugin.json到TypeScript SDK的实战指南

1. 从“plugins”这个词说起:它到底在解决什么问题但凡折腾过现代开发工具的人,对plugins这个词都不会陌生。它字面意思就是“插件”,但真正理解它的人知道,这背后其实是一整套可扩展架构的设计哲学。你用的编辑器、IDE、命令行工…

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑