Implementing Fallback Error Pages in Nginx with Proper POST Request Handling


9 views

When configuring Nginx to handle error pages with fallback locations, many developers encounter a specific challenge: the system works perfectly for GET requests but fails when handling POST requests that result in errors. This occurs because Nginx preserves the original request method when processing error_page directives.

server {
    root /var/www/someserver.com;
    
    location ~* ^/(robots\.txt)$ {
        error_page 404 = @default;
    }

    location @default {
        root /var/www/default;
    }
}

For error pages, a naive implementation might look like this:

server {
    root /var/www/someserver.com;
    
    error_page 404 /404.html;
    
    location ~* ^/(404\.html)$ {
        error_page 404 = @default;
    }
    
    location @default {
        root /var/www/default;
    }
}

This works for direct requests to /404.html but fails for actual 404 errors because Nginx's error_page directive doesn't properly chain to the fallback location.

Here's the complete solution that handles both GET and POST requests correctly:

server {
    root /var/www/someserver.com;
    
    # Handle errors via named locations
    error_page 404 = @notfound;
    error_page 500 502 504 = @server_error;
    error_page 503 = @maintenance;
    
    location @notfound {
        # Convert POST to GET for static files
        if ($request_method = POST) {
            return 307 /404.html;
        }
        try_files /404.html /../default/404.html =404;
    }
    
    location @server_error {
        if ($request_method = POST) {
            return 307 /500.html;
        }
        try_files /500.html /../default/500.html =500;
    }
    
    location @maintenance {
        if ($request_method = POST) {
            return 307 /503.html;
        }
        try_files /503.html /../default/503.html =503;
    }
    
    # Handle direct requests to error pages
    location ~* ^/(40[34]|50[0345])\.html$ {
        try_files $uri /../default/$uri;
    }
}

1. The 307 redirect preserves POST data while changing to GET method
2. Named locations (@notfound, etc.) provide clean error handling
3. The separate location block handles direct requests to error pages
4. Relative paths (/../default/) make the configuration more portable

For those who prefer not to use redirects:

map $request_method $error_page_method {
    default "GET";
    POST "GET";
}

server {
    # ... other config ...

    location @notfound {
        limit_except GET { deny all; }
        try_files /404.html /../default/404.html =404;
    }
}

This approach uses Nginx's map directive to force GET method for error pages.


When configuring nginx to serve custom error pages, we often want a hierarchical fallback system where server-specific error pages take precedence over default ones. While this works perfectly for static files like robots.txt, implementing the same behavior for error pages presents unique challenges, particularly with different HTTP request methods.

For standard GET requests, this configuration works well:

server {
    root /var/www/someserver.com;
    
    error_page  404         = @notfound;
    error_page  500 502 504 = @server_error;
    
    location @notfound {
        try_files /404.html /../default/404.html =404;
    }
    
    location @server_error {
        try_files /500.html /../default/500.html =500;
    }
}

The critical limitation appears when handling POST requests that result in errors. Nginx forwards the original request method when processing the error page, which leads to 405 Not Allowed errors when trying to serve static HTML files via POST.

Here's an improved version that properly handles all request methods:

server {
    root /var/www/someserver.com;
    
    # Primary error page configuration
    error_page 404 /404.html;
    error_page 500 502 503 504 /500.html;
    
    # Fallback logic for error pages
    location = /404.html {
        internal;
        try_files /404.html /../default/404.html;
    }
    
    location = /500.html {
        internal;
        try_files /500.html /../default/500.html;
    }
    
    # Special handling for maintenance mode
    location = /503.html {
        internal;
        try_files /503.html /../default/503.html;
    }
}
  • The internal directive prevents direct external access to error pages
  • Relative paths using ../default/ work well when included in multiple server blocks
  • Each error page has its own dedicated location block for precise control

Always verify your configuration with both GET and POST requests:

# Test GET request to non-existent URL
curl -I http://yourserver.com/nonexistent-page

# Test POST request that will generate error
curl -X POST http://yourserver.com/nonexistent-api-endpoint

For high-traffic sites, consider these optimizations:

location ~* \.(html)$ {
    expires 1h;
    add_header Cache-Control "public";
    etag off;
}