Nginx 修改默认错误页面:实现带 CSS 动画的自定义错误页
一、背景
Nginx 默认的错误页面非常简单,只有一个纯文本的提示,如下图。这对于用户体验不够友好,特别是在面向终端用户的服务中,一个美观的错误页面能够大大提升产品的专业度。

本文将介绍如何修改 Nginx 源码,将默认的错误页面替换为带有 CSS 动画效果的自定义 HTML 页面。
二、Nginx 错误页面的存储方式
Nginx 将错误页面以静态字符串数组的形式定义在 src/http/ngx_http_special_response.c 文件中。每个 HTTP 状态码对应一个静态字符数组:
c
static char ngx_http_error_404_page[] =
"<!DOCTYPE html>" CRLF
"<html>" CRLF
"<head><title>404 Not Found</title></head>" CRLF
"<body>" CRLF
"<center><h1>404 Not Found</h1></center>" CRLF
"</body>" CRLF
"</html>" CRLF;
三、需求分析
我们希望实现一个通用的错误页面模板,具有以下特点:
- 美观的 CSS 样式:现代化的视觉效果,响应式设计
- 数字动画效果:错误码的三个数字位分别带有弹跳动画
- 易于维护:使用宏定义避免重复代码
- 编译时确定:能够静态初始化,不影响运行时性能
四、技术难点与解决方案
4.1 问题:宏中不能进行运算
最初尝试使用宏直接拆分数字:
c
// ❌ 错误示例
#define NGX_HTTP_ERROR_PAGE_TITLE(code, text) \
"<div class=\"title\">" #code/100 "<span>" #code/10%10 "</span>" #code%10 "</div>"
问题 :#code 会将参数字符串化,不能对其结果进行运算。
4.2 解决方案:手动拆分数字位
将数字的百位、十位、个位分别作为宏参数传入:
c
#define NGX_HTTP_ERROR_PAGE_TITLE(code1, code2, code3, text) \
"<div class=\"title\">" #code1 "<span>" #code2 "</span>" #code3 "</div>"
这样每个数字位独立使用 # 运算符转换为字符串,可以用于静态数组初始化。
五、完整实现代码
5.1 定义宏
需要增加一个宏定义,来实现错误页面通用模板,不用每个 HTTP 状态码对应的静态字符数组都重复写内容。
5.2 错误页面模板宏
c
#define NGX_HTTP_ERROR_PAGE_TITLE(code1, code2, code3, text) \
"<!DOCTYPE html>" CRLF \
"<html lang=\"zh-CN\">" CRLF \
"<head>" CRLF \
" <meta charset=\"UTF-8\">" CRLF \
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">" CRLF \
" <title>" text "</title>" CRLF \
" <style>" CRLF \
" *{margin:0;padding:0;box-sizing:border-box;font-family:'Segoe UI','Microsoft YaHei',sans-serif}" CRLF \
" body{background:#fff;min-height:100vh;display:flex;justify-content:center;align-items:center;overflow:hidden}" CRLF \
" .error-container{text-align:center;padding:20px;max-width:700px;width:90%}" CRLF \
" .error-body{background:#fff;border-radius:10px;padding:60px 40px 40px;position:relative}" CRLF \
" .error-code{font-size:150px;font-weight:700;color:#2d8cf0;height:260px;line-height:260px;text-shadow:0 0 15px rgba(45,140,240,.6);margin-top:40px}" CRLF \
" .error-code span{display:inline-block;color:#19be6b;font-size:150px;animation:bounce 3s ease 0s infinite alternate;transform-origin:center}" CRLF \
" @keyframes bounce{0%{transform:rotateZ(0)}" CRLF \
" 20%{transform:rotateZ(-60deg)}" CRLF \
" 40%{transform:rotateZ(-10deg)}" CRLF \
" 60%{transform:rotateZ(50deg)}" CRLF \
" 80%{transform:rotateZ(-20deg)}" CRLF \
" 100%{transform:rotateZ(0)}" CRLF \
" }" CRLF \
" .error-message{display:block;text-align:center;font-size:20px;font-weight:500;letter-spacing:5px;color:#dddde2}" CRLF \
" @media (max-width:768px){.error-code{font-size:120px;height:200px;line-height:200px}" CRLF \
" .error-code span{font-size:120px}" CRLF \
" }" CRLF \
" @media (max-width:480px){.error-code{font-size:90px;height:150px;line-height:150px}" CRLF \
" .error-code span{font-size:90px}" CRLF \
" .error-message{font-size:16px;letter-spacing:4px}" CRLF \
" }" CRLF \
" </style>" CRLF \
"</head>" CRLF \
"<body>" CRLF \
" <div class=\"error-container\">" CRLF \
" <div class=\"error-body\">" CRLF \
" <div class=\"error-code\">" #code1 "<span>" #code2 "</span>" #code3 "</div>" CRLF \
" <p class=\"error-message\">" text "</p>" CRLF \
" </div>" CRLF \
" </div>" CRLF \
"</body>" CRLF \
"</html>"
5.3 为各状态码生成错误页面
修改原来的各状态码错误页面定义部分,调用我们定义的宏:
c
/* 3xx 重定向错误 */
static char ngx_http_error_301_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,1, "301 Moved Permanently");
static char ngx_http_error_302_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,2, "302 Found");
static char ngx_http_error_303_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,3, "303 See Other");
static char ngx_http_error_307_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,7, "307 Temporary Redirect");
static char ngx_http_error_308_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,8, "308 Permanent Redirect");
/* 4xx 客户端错误 */
static char ngx_http_error_400_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,0, "400 Bad Request");
static char ngx_http_error_401_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,1, "401 Authorization Required");
static char ngx_http_error_403_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,3, "403 Forbidden");
static char ngx_http_error_404_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,4, "404 Not Found");
static char ngx_http_error_405_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,5, "405 Not Allowed");
static char ngx_http_error_406_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,6, "406 Not Acceptable");
static char ngx_http_error_407_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,7, "407 Proxy Authentication Required");
static char ngx_http_error_408_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,8, "408 Request Time-out");
static char ngx_http_error_409_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,9, "409 Conflict");
static char ngx_http_error_410_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,0, "410 Gone");
static char ngx_http_error_411_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,1, "411 Length Required");
static char ngx_http_error_412_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,2, "412 Precondition Failed");
static char ngx_http_error_413_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,3, "413 Request Entity Too Large");
static char ngx_http_error_414_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,4, "414 Request-URI Too Large");
static char ngx_http_error_415_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,5, "415 Unsupported Media Type");
static char ngx_http_error_416_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,6, "416 Requested Range Not Satisfiable");
static char ngx_http_error_421_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,2,1, "421 Misdirected Request");
static char ngx_http_error_429_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,2,9, "429 Too Many Requests");
static char ngx_http_error_494_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,9,4, "400 Request Header Or Cookie Too Large");
static char ngx_http_error_495_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,9,5, "400 The SSL certificate error");
static char ngx_http_error_496_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,9,6, "400 No required SSL certificate was sent");
static char ngx_http_error_497_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,9,7, "400 The plain HTTP request was sent to HTTPS port");
/* 5xx 服务端错误 */
static char ngx_http_error_500_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,0, "500 Internal Server Error");
static char ngx_http_error_501_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,1, "501 Not Implemented");
static char ngx_http_error_502_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,2, "502 Bad Gateway");
static char ngx_http_error_503_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,3, "503 Service Temporarily Unavailable");
static char ngx_http_error_504_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,4, "504 Gateway Time-out");
static char ngx_http_error_505_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,5, "505 HTTP Version Not Supported");
static char ngx_http_error_507_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,7, "507 Insufficient Storage");
5.4 修改后的完整代码
修改后的完整ngx_http_special_response.c代码,只做增强,没有改变原来架构逻辑:
c
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
*/
#include <ngx_config.h>
#include <ngx_core.h>
#include <ngx_http.h>
#include <nginx.h>
static ngx_int_t ngx_http_send_error_page(ngx_http_request_t *r,
ngx_http_err_page_t *err_page);
static ngx_int_t ngx_http_send_special_response(ngx_http_request_t *r,
ngx_http_core_loc_conf_t *clcf, ngx_uint_t err);
static ngx_int_t ngx_http_send_refresh(ngx_http_request_t *r);
static u_char ngx_http_error_full_tail[] =
"" CRLF
;
static u_char ngx_http_error_build_tail[] =
"" CRLF
;
static u_char ngx_http_error_tail[] =
"" CRLF
;
static u_char ngx_http_msie_padding[] =
"<!-- a padding to disable MSIE and Chrome friendly error page -->" CRLF
"<!-- a padding to disable MSIE and Chrome friendly error page -->" CRLF
"<!-- a padding to disable MSIE and Chrome friendly error page -->" CRLF
"<!-- a padding to disable MSIE and Chrome friendly error page -->" CRLF
"<!-- a padding to disable MSIE and Chrome friendly error page -->" CRLF
"<!-- a padding to disable MSIE and Chrome friendly error page -->" CRLF
;
static u_char ngx_http_msie_refresh_head[] =
"<html><head><meta http-equiv=\"Refresh\" content=\"0; URL=";
static u_char ngx_http_msie_refresh_tail[] =
"\"></head><body></body></html>" CRLF;
/* 定义模板宏 通用错误页面模板 */
#define NGX_HTTP_ERROR_PAGE_TITLE(code1,code2,code3, text) \
"<!DOCTYPE html>" CRLF \
"<html lang=\"zh-CN\">" CRLF \
"<head>" CRLF \
" <meta charset=\"UTF-8\">" CRLF \
" <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">" CRLF \
" <title>" text "</title>" CRLF \
" <style>" CRLF \
" *{margin:0;padding:0;box-sizing:border-box;font-family:'Segoe UI','Microsoft YaHei',sans-serif}" CRLF \
" body{background:#fff;min-height:100vh;display:flex;justify-content:center;align-items:center;overflow:hidden}" CRLF \
" .error404{text-align:center;padding:20px;max-width:700px;width:90%}" CRLF \
" .error404-body-con{background:#fff;border-radius:10px;padding:60px 40px 40px;position:relative}" CRLF \
" .error404-body-con-title{font-size:150px;font-weight:700;color:#2d8cf0;height:260px;line-height:260px;text-shadow:0 0 15px rgba(45,140,240,.6);margin-top:40px}" CRLF \
" .error404-body-con-title span{display:inline-block;color:#19be6b;font-size:150px;animation:error404animation 3s ease 0s infinite alternate;transform-origin:center}" CRLF \
" @keyframes error404animation{0%{transform:rotateZ(0)}" CRLF \
" 20%{transform:rotateZ(-60deg)}" CRLF \
" 40%{transform:rotateZ(-10deg)}" CRLF \
" 60%{transform:rotateZ(50deg)}" CRLF \
" 80%{transform:rotateZ(-20deg)}" CRLF \
" 100%{transform:rotateZ(0)}" CRLF \
" }" CRLF \
" .error404-body-con-message{display:block;text-align:center;font-size:20px;font-weight:500;letter-spacing:5px;color:#dddde2}" CRLF \
" @media (max-width:768px){.error404-body-con-title{font-size:120px;height:200px;line-height:200px}" CRLF \
" .error404-body-con-title span{font-size:120px}" CRLF \
" }" CRLF \
" @media (max-width:480px){.error404-body-con-title{font-size:90px;height:150px;line-height:150px}" CRLF \
" .error404-body-con-title span{font-size:90px}" CRLF \
" .error404-body-con-message{font-size:16px;letter-spacing:4px}" CRLF \
" }" CRLF \
" </style>" CRLF \
"</head>" CRLF \
"<body>" CRLF \
" <div class=\"error404\">" CRLF \
" <div class=\"error404-body-con\">" CRLF \
" <div class=\"error404-body-con-title\">" #code1 "<span>" #code2 "</span>" #code3 "</div>" CRLF \
" <p class=\"error404-body-con-message\">" text "</p>" CRLF \
" </div>" CRLF \
" </div>" CRLF \
"</body>" CRLF \
"</html>"
/* 为每个状态码生成页面 */
static char ngx_http_error_301_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,1, "301 Moved Permanently");
static char ngx_http_error_302_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,2, "302 Found");
static char ngx_http_error_303_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,3, "303 See Other");
static char ngx_http_error_307_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,7, "307 Temporary Redirect");
static char ngx_http_error_308_page[] = NGX_HTTP_ERROR_PAGE_TITLE(3,0,8, "308 Permanent Redirect");
static char ngx_http_error_400_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,0, "400 Bad Request");
static char ngx_http_error_401_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,1, "401 Authorization Required");
static char ngx_http_error_402_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,2, "402 Payment Required");
static char ngx_http_error_403_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,3, "403 Forbidden");
static char ngx_http_error_404_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,4, "404 Not Found");
static char ngx_http_error_405_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,5, "405 Not Allowed");
static char ngx_http_error_406_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,6, "406 Not Acceptable");
static char ngx_http_error_407_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,7, "407 Proxy Authentication Required");
static char ngx_http_error_408_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,8, "408 Request Time-out");
static char ngx_http_error_409_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,0,9, "409 Conflict");
static char ngx_http_error_410_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,0, "410 Gone");
static char ngx_http_error_411_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,1, "411 Length Required");
static char ngx_http_error_412_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,2, "412 Precondition Failed");
static char ngx_http_error_413_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,3, "413 Request Entity Too Large");
static char ngx_http_error_414_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,4, "414 Request-URI Too Large");
static char ngx_http_error_415_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,5, "415 Unsupported Media Type");
static char ngx_http_error_416_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,1,6, "416 Requested Range Not Satisfiable");
static char ngx_http_error_421_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,2,1, "421 Misdirected Request");
static char ngx_http_error_429_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,2,9, "429 Too Many Requests");
static char ngx_http_error_494_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,9,4, "400 Request Header Or Cookie Too Large");
static char ngx_http_error_495_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,9,5, "400 The SSL certificate error");
static char ngx_http_error_496_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,9,6, "400 No required SSL certificate was sent");
static char ngx_http_error_497_page[] = NGX_HTTP_ERROR_PAGE_TITLE(4,9,7, "400 The plain HTTP request was sent to HTTPS port");
static char ngx_http_error_500_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,0, "500 Internal Server Error");
static char ngx_http_error_501_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,1, "501 Not Implemented");
static char ngx_http_error_502_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,2, "502 Bad Gateway");
static char ngx_http_error_503_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,3, "503 Service Temporarily Unavailable");
static char ngx_http_error_504_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,4, "504 Gateway Time-out");
static char ngx_http_error_505_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,5, "505 HTTP Version Not Supported");
static char ngx_http_error_507_page[] = NGX_HTTP_ERROR_PAGE_TITLE(5,0,7, "507 Insufficient Storage");
static ngx_str_t ngx_http_error_pages[] = {
ngx_null_string, /* 201, 204 */
#define NGX_HTTP_LAST_2XX 202
#define NGX_HTTP_OFF_3XX (NGX_HTTP_LAST_2XX - 201)
/* ngx_null_string, */ /* 300 */
ngx_string(ngx_http_error_301_page),
ngx_string(ngx_http_error_302_page),
ngx_string(ngx_http_error_303_page),
ngx_null_string, /* 304 */
ngx_null_string, /* 305 */
ngx_null_string, /* 306 */
ngx_string(ngx_http_error_307_page),
ngx_string(ngx_http_error_308_page),
#define NGX_HTTP_LAST_3XX 309
#define NGX_HTTP_OFF_4XX (NGX_HTTP_LAST_3XX - 301 + NGX_HTTP_OFF_3XX)
ngx_string(ngx_http_error_400_page),
ngx_string(ngx_http_error_401_page),
ngx_string(ngx_http_error_402_page),
ngx_string(ngx_http_error_403_page),
ngx_string(ngx_http_error_404_page),
ngx_string(ngx_http_error_405_page),
ngx_string(ngx_http_error_406_page),
ngx_string(ngx_http_error_407_page),
ngx_string(ngx_http_error_408_page),
ngx_string(ngx_http_error_409_page),
ngx_string(ngx_http_error_410_page),
ngx_string(ngx_http_error_411_page),
ngx_string(ngx_http_error_412_page),
ngx_string(ngx_http_error_413_page),
ngx_string(ngx_http_error_414_page),
ngx_string(ngx_http_error_415_page),
ngx_string(ngx_http_error_416_page),
ngx_null_string, /* 417 */
ngx_null_string, /* 418 */
ngx_null_string, /* 419 */
ngx_null_string, /* 420 */
ngx_string(ngx_http_error_421_page),
ngx_null_string, /* 422 */
ngx_null_string, /* 423 */
ngx_null_string, /* 424 */
ngx_null_string, /* 425 */
ngx_null_string, /* 426 */
ngx_null_string, /* 427 */
ngx_null_string, /* 428 */
ngx_string(ngx_http_error_429_page),
#define NGX_HTTP_LAST_4XX 430
#define NGX_HTTP_OFF_5XX (NGX_HTTP_LAST_4XX - 400 + NGX_HTTP_OFF_4XX)
ngx_string(ngx_http_error_494_page), /* 494, request header too large */
ngx_string(ngx_http_error_495_page), /* 495, https certificate error */
ngx_string(ngx_http_error_496_page), /* 496, https no certificate */
ngx_string(ngx_http_error_497_page), /* 497, http to https */
ngx_string(ngx_http_error_404_page), /* 498, canceled */
ngx_null_string, /* 499, client has closed connection */
ngx_string(ngx_http_error_500_page),
ngx_string(ngx_http_error_501_page),
ngx_string(ngx_http_error_502_page),
ngx_string(ngx_http_error_503_page),
ngx_string(ngx_http_error_504_page),
ngx_string(ngx_http_error_505_page),
ngx_null_string, /* 506 */
ngx_string(ngx_http_error_507_page)
#define NGX_HTTP_LAST_5XX 508
};
ngx_int_t
ngx_http_special_response_handler(ngx_http_request_t *r, ngx_int_t error)
{
ngx_uint_t i, err;
ngx_http_err_page_t *err_page;
ngx_http_core_loc_conf_t *clcf;
ngx_log_debug3(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
"http special response: %i, \"%V?%V\"",
error, &r->uri, &r->args);
r->err_status = error;
if (r->keepalive) {
switch (error) {
case NGX_HTTP_BAD_REQUEST:
case NGX_HTTP_REQUEST_ENTITY_TOO_LARGE:
case NGX_HTTP_REQUEST_URI_TOO_LARGE:
case NGX_HTTP_TO_HTTPS:
case NGX_HTTPS_CERT_ERROR:
case NGX_HTTPS_NO_CERT:
case NGX_HTTP_INTERNAL_SERVER_ERROR:
case NGX_HTTP_NOT_IMPLEMENTED:
r->keepalive = 0;
}
}
if (r->lingering_close) {
switch (error) {
case NGX_HTTP_BAD_REQUEST:
case NGX_HTTP_TO_HTTPS:
case NGX_HTTPS_CERT_ERROR:
case NGX_HTTPS_NO_CERT:
r->lingering_close = 0;
}
}
r->headers_out.content_type.len = 0;
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
if (!r->error_page && clcf->error_pages && r->uri_changes != 0) {
if (clcf->recursive_error_pages == 0) {
r->error_page = 1;
}
err_page = clcf->error_pages->elts;
for (i = 0; i < clcf->error_pages->nelts; i++) {
if (err_page[i].status == error) {
return ngx_http_send_error_page(r, &err_page[i]);
}
}
}
r->expect_tested = 1;
if (ngx_http_discard_request_body(r) != NGX_OK) {
r->keepalive = 0;
}
if (clcf->msie_refresh
&& r->headers_in.msie
&& (error == NGX_HTTP_MOVED_PERMANENTLY
|| error == NGX_HTTP_MOVED_TEMPORARILY))
{
return ngx_http_send_refresh(r);
}
if (error == NGX_HTTP_CREATED) {
/* 201 */
err = 0;
} else if (error == NGX_HTTP_NO_CONTENT) {
/* 204 */
err = 0;
} else if (error >= NGX_HTTP_MOVED_PERMANENTLY
&& error < NGX_HTTP_LAST_3XX)
{
/* 3XX */
err = error - NGX_HTTP_MOVED_PERMANENTLY + NGX_HTTP_OFF_3XX;
} else if (error >= NGX_HTTP_BAD_REQUEST
&& error < NGX_HTTP_LAST_4XX)
{
/* 4XX */
err = error - NGX_HTTP_BAD_REQUEST + NGX_HTTP_OFF_4XX;
} else if (error >= NGX_HTTP_NGINX_CODES
&& error < NGX_HTTP_LAST_5XX)
{
/* 49X, 5XX */
err = error - NGX_HTTP_NGINX_CODES + NGX_HTTP_OFF_5XX;
switch (error) {
case NGX_HTTP_TO_HTTPS:
case NGX_HTTPS_CERT_ERROR:
case NGX_HTTPS_NO_CERT:
case NGX_HTTP_REQUEST_HEADER_TOO_LARGE:
r->err_status = NGX_HTTP_BAD_REQUEST;
}
} else {
/* unknown code, zero body */
err = 0;
}
return ngx_http_send_special_response(r, clcf, err);
}
ngx_int_t
ngx_http_filter_finalize_request(ngx_http_request_t *r, ngx_module_t *m,
ngx_int_t error)
{
void *ctx;
ngx_int_t rc;
ngx_http_clean_header(r);
ctx = NULL;
if (m) {
ctx = r->ctx[m->ctx_index];
}
/* clear the modules contexts */
ngx_memzero(r->ctx, sizeof(void *) * ngx_http_max_module);
if (m) {
r->ctx[m->ctx_index] = ctx;
}
r->filter_finalize = 1;
rc = ngx_http_special_response_handler(r, error);
/* NGX_ERROR resets any pending data */
switch (rc) {
case NGX_OK:
case NGX_DONE:
return NGX_ERROR;
default:
return rc;
}
}
void
ngx_http_clean_header(ngx_http_request_t *r)
{
ngx_memzero(&r->headers_out.status,
sizeof(ngx_http_headers_out_t)
- offsetof(ngx_http_headers_out_t, status));
r->headers_out.headers.part.nelts = 0;
r->headers_out.headers.part.next = NULL;
r->headers_out.headers.last = &r->headers_out.headers.part;
r->headers_out.trailers.part.nelts = 0;
r->headers_out.trailers.part.next = NULL;
r->headers_out.trailers.last = &r->headers_out.trailers.part;
r->headers_out.content_length_n = -1;
r->headers_out.last_modified_time = -1;
}
static ngx_int_t
ngx_http_send_error_page(ngx_http_request_t *r, ngx_http_err_page_t *err_page)
{
ngx_int_t overwrite;
ngx_str_t uri, args;
ngx_table_elt_t *location;
ngx_http_core_loc_conf_t *clcf;
overwrite = err_page->overwrite;
if (overwrite && overwrite != NGX_HTTP_OK) {
r->expect_tested = 1;
}
if (overwrite >= 0) {
r->err_status = overwrite;
}
if (ngx_http_complex_value(r, &err_page->value, &uri) != NGX_OK) {
return NGX_ERROR;
}
if (uri.len && uri.data[0] == '/') {
if (err_page->value.lengths) {
ngx_http_split_args(r, &uri, &args);
} else {
args = err_page->args;
}
if (r->method != NGX_HTTP_HEAD) {
r->method = NGX_HTTP_GET;
r->method_name = ngx_http_core_get_method;
}
return ngx_http_internal_redirect(r, &uri, &args);
}
if (uri.len && uri.data[0] == '@') {
return ngx_http_named_location(r, &uri);
}
r->expect_tested = 1;
if (ngx_http_discard_request_body(r) != NGX_OK) {
r->keepalive = 0;
}
location = ngx_list_push(&r->headers_out.headers);
if (location == NULL) {
return NGX_ERROR;
}
if (overwrite != NGX_HTTP_MOVED_PERMANENTLY
&& overwrite != NGX_HTTP_MOVED_TEMPORARILY
&& overwrite != NGX_HTTP_SEE_OTHER
&& overwrite != NGX_HTTP_TEMPORARY_REDIRECT
&& overwrite != NGX_HTTP_PERMANENT_REDIRECT)
{
r->err_status = NGX_HTTP_MOVED_TEMPORARILY;
}
location->hash = 1;
location->next = NULL;
ngx_str_set(&location->key, "Location");
location->value = uri;
ngx_http_clear_location(r);
r->headers_out.location = location;
clcf = ngx_http_get_module_loc_conf(r, ngx_http_core_module);
if (clcf->msie_refresh && r->headers_in.msie) {
return ngx_http_send_refresh(r);
}
return ngx_http_send_special_response(r, clcf, r->err_status
- NGX_HTTP_MOVED_PERMANENTLY
+ NGX_HTTP_OFF_3XX);
}
static ngx_int_t
ngx_http_send_special_response(ngx_http_request_t *r,
ngx_http_core_loc_conf_t *clcf, ngx_uint_t err)
{
u_char *tail;
size_t len;
ngx_int_t rc;
ngx_buf_t *b;
ngx_uint_t msie_padding;
ngx_chain_t out[3];
if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_ON) {
len = sizeof(ngx_http_error_full_tail) - 1;
tail = ngx_http_error_full_tail;
} else if (clcf->server_tokens == NGX_HTTP_SERVER_TOKENS_BUILD) {
len = sizeof(ngx_http_error_build_tail) - 1;
tail = ngx_http_error_build_tail;
} else {
len = sizeof(ngx_http_error_tail) - 1;
tail = ngx_http_error_tail;
}
msie_padding = 0;
if (ngx_http_error_pages[err].len) {
r->headers_out.content_length_n = ngx_http_error_pages[err].len + len;
if (clcf->msie_padding
&& (r->headers_in.msie || r->headers_in.chrome)
&& r->http_version >= NGX_HTTP_VERSION_10
&& err >= NGX_HTTP_OFF_4XX)
{
r->headers_out.content_length_n +=
sizeof(ngx_http_msie_padding) - 1;
msie_padding = 1;
}
r->headers_out.content_type_len = sizeof("text/html") - 1;
ngx_str_set(&r->headers_out.content_type, "text/html");
r->headers_out.content_type_lowcase = NULL;
} else {
r->headers_out.content_length_n = 0;
}
if (r->headers_out.content_length) {
r->headers_out.content_length->hash = 0;
r->headers_out.content_length = NULL;
}
ngx_http_clear_accept_ranges(r);
ngx_http_clear_last_modified(r);
ngx_http_clear_etag(r);
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || r->header_only) {
return rc;
}
if (ngx_http_error_pages[err].len == 0) {
return ngx_http_send_special(r, NGX_HTTP_LAST);
}
b = ngx_calloc_buf(r->pool);
if (b == NULL) {
return NGX_ERROR;
}
b->memory = 1;
b->pos = ngx_http_error_pages[err].data;
b->last = ngx_http_error_pages[err].data + ngx_http_error_pages[err].len;
out[0].buf = b;
out[0].next = &out[1];
b = ngx_calloc_buf(r->pool);
if (b == NULL) {
return NGX_ERROR;
}
b->memory = 1;
b->pos = tail;
b->last = tail + len;
out[1].buf = b;
out[1].next = NULL;
if (msie_padding) {
b = ngx_calloc_buf(r->pool);
if (b == NULL) {
return NGX_ERROR;
}
b->memory = 1;
b->pos = ngx_http_msie_padding;
b->last = ngx_http_msie_padding + sizeof(ngx_http_msie_padding) - 1;
out[1].next = &out[2];
out[2].buf = b;
out[2].next = NULL;
}
if (r == r->main) {
b->last_buf = 1;
}
b->last_in_chain = 1;
return ngx_http_output_filter(r, &out[0]);
}
static ngx_int_t
ngx_http_send_refresh(ngx_http_request_t *r)
{
u_char *p, *location;
size_t len, size;
uintptr_t escape;
ngx_int_t rc;
ngx_buf_t *b;
ngx_chain_t out;
len = r->headers_out.location->value.len;
location = r->headers_out.location->value.data;
escape = 2 * ngx_escape_uri(NULL, location, len, NGX_ESCAPE_REFRESH);
size = sizeof(ngx_http_msie_refresh_head) - 1
+ escape + len
+ sizeof(ngx_http_msie_refresh_tail) - 1;
r->err_status = NGX_HTTP_OK;
r->headers_out.content_type_len = sizeof("text/html") - 1;
ngx_str_set(&r->headers_out.content_type, "text/html");
r->headers_out.content_type_lowcase = NULL;
r->headers_out.location->hash = 0;
r->headers_out.location = NULL;
r->headers_out.content_length_n = size;
if (r->headers_out.content_length) {
r->headers_out.content_length->hash = 0;
r->headers_out.content_length = NULL;
}
ngx_http_clear_accept_ranges(r);
ngx_http_clear_last_modified(r);
ngx_http_clear_etag(r);
rc = ngx_http_send_header(r);
if (rc == NGX_ERROR || r->header_only) {
return rc;
}
b = ngx_create_temp_buf(r->pool, size);
if (b == NULL) {
return NGX_ERROR;
}
p = ngx_cpymem(b->pos, ngx_http_msie_refresh_head,
sizeof(ngx_http_msie_refresh_head) - 1);
if (escape == 0) {
p = ngx_cpymem(p, location, len);
} else {
p = (u_char *) ngx_escape_uri(p, location, len, NGX_ESCAPE_REFRESH);
}
b->last = ngx_cpymem(p, ngx_http_msie_refresh_tail,
sizeof(ngx_http_msie_refresh_tail) - 1);
b->last_buf = (r == r->main) ? 1 : 0;
b->last_in_chain = 1;
out.buf = b;
out.next = NULL;
return ngx_http_output_filter(r, &out);
}
六、实现效果
修改完成后,重新编译 Nginx:
bash
./configure --prefix=/usr/local/nginx \
--conf-path=/usr/local/nginx/etc/nginx.conf \
--sbin-path=/usr/local/nginx/sbin/nginx \
--user=nginx \
--group=nginx \
--with-pcre \
--with-http_ssl_module \
--with-http_v2_module \
--with-http_v3_module \
--with-http_realip_module \
--with-http_addition_module \
--with-http_image_filter_module \
--with-http_sub_module \
--with-http_dav_module \
--with-http_flv_module \
--with-http_mp4_module \
--with-http_gzip_static_module \
--with-http_random_index_module \
--with-http_secure_link_module \
--with-http_degradation_module \
--with-http_stub_status_module \
--with-stream \
--with-mail
make
make install
访问一个不存在的页面,你将看到带有 CSS 动画效果的自定义错误页面(效果如下图):
- 错误码的每个数字位独立显示
- 中间的数字(十位)带有旋转弹跳动画
- 响应式设计,在移动端自动调整字体大小
- 现代化的视觉风格,带阴影和圆角

七、技术要点总结
- 宏的参数化设计:通过将复杂逻辑拆分为多个参数,避开了宏的运算限制
- 静态初始化:确保所有字符串在编译时确定,不影响运行时性能
- CSS 动画:使用 CSS3 keyframes 实现数字弹跳效果
- 响应式设计:通过媒体查询适配不同屏幕尺寸
八、扩展思考
8.1 动态错误页面
如果需要更灵活的错误页面(如从文件读取模板),可以考虑:
c
static char *ngx_http_error_page_template = NULL;
static char* ngx_http_get_error_page(ngx_http_request_t *r, int code) {
// 从配置文件读取模板,动态生成
}
8.2 性能考虑
静态字符串数组的优点:
- 零运行时开销
- 直接发送,无需额外内存分配
- CPU Cache 友好
动态生成的优点:
- 灵活性高,可支持更多定制
- 便于维护和更新
根据实际需求选择合适的方案。
九、参考资料
总结:通过巧妙地使用 C 语言宏,在不破坏 Nginx 原有架构的前提下,为所有错误页面统一添加了现代化的 CSS 样式和动画效果。这种方法既保持了高性能,又提升了用户体验,是一个典型的"零成本抽象"实践。