Nginx 修改默认错误页面:实现带 CSS 动画的自定义错误页

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;

三、需求分析

我们希望实现一个通用的错误页面模板,具有以下特点:

  1. 美观的 CSS 样式:现代化的视觉效果,响应式设计
  2. 数字动画效果:错误码的三个数字位分别带有弹跳动画
  3. 易于维护:使用宏定义避免重复代码
  4. 编译时确定:能够静态初始化,不影响运行时性能

四、技术难点与解决方案

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 动画效果的自定义错误页面(效果如下图):

  • 错误码的每个数字位独立显示
  • 中间的数字(十位)带有旋转弹跳动画
  • 响应式设计,在移动端自动调整字体大小
  • 现代化的视觉风格,带阴影和圆角

七、技术要点总结

  1. 宏的参数化设计:通过将复杂逻辑拆分为多个参数,避开了宏的运算限制
  2. 静态初始化:确保所有字符串在编译时确定,不影响运行时性能
  3. CSS 动画:使用 CSS3 keyframes 实现数字弹跳效果
  4. 响应式设计:通过媒体查询适配不同屏幕尺寸

八、扩展思考

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 样式和动画效果。这种方法既保持了高性能,又提升了用户体验,是一个典型的"零成本抽象"实践。

相关推荐
wanhengidc1 小时前
云手机 游戏多开不卡顿
运维·服务器·网络·安全·web安全·游戏·智能手机
TEC_INO1 小时前
Linux58:rockx_vi_handle_thread线程的讲解
linux·运维·服务器
袁煦丞 cpolar内网穿透实验室2 小时前
出差路上,服务器在我手机里
运维·服务器·docker·容器·智能手机·远程工作·cpolar
小此方2 小时前
Re:Linux系统篇(十三)特别篇: 实现Linux第⼀个系统程序−进度条
linux·运维·服务器
夏日听雨眠10 小时前
LInux(逻辑地址与物理地址的区别,文件描述符,lseek函数)
linux·运维·网络
哲霖软件11 小时前
ERP 赋能非标自动化行业:破解物料与库存管理难题
运维·自动化
火车叼位12 小时前
替代 Tiny Win10 的 Linux 方案:Debian XFCE 精简桌面搭建
linux·运维
syagain_zsx14 小时前
Linux指令初识(实用篇)
linux·运维·服务器
OYangxf14 小时前
Git Commit Message
运维·git