yum upgrades for production use, this is the repository for you.
Active subscription is required.
Your API returns 404 for soft-deleted resources, but search engines need 410 Gone to stop crawling them. Your upstream sends 502 Bad Gateway during deploys, but clients should see 503 Service Unavailable with a retry hint. Your legacy backend leaks internal status codes that expose your infrastructure. You need a way to have NGINX rewrite response status code values on the fly — but NGINX has no native way to do it without losing the response body.
The built-in error_page directive can intercept errors, but it triggers an internal redirect that discards the upstream body entirely. You lose the JSON error payload, the HTML maintenance page from the backend, or whatever the upstream sent.
The solution: the rewrite_status module. It changes the HTTP status code of any NGINX response — including 2xx success codes — directly in the output filter chain. The original response body and headers stay intact. No internal redirects. No re-executed request phases. Just a clean status code swap.
Why Not Use error_page?
Before reaching for a third-party module, understand what native NGINX can and cannot do when you need to rewrite a response status code.
The error_page directive combined with proxy_intercept_errors is the standard approach:
proxy_intercept_errors on;
error_page 502 503 /maintenance.html;
error_page 404 =410 /gone.html;
However, this has significant limitations:
| Aspect | error_page |
rewrite_status |
|---|---|---|
| Response body | Discarded — replaced by redirect target | Preserved from upstream |
| Applicable codes | Only 3xx–5xx error responses | Any status code (100–999) |
| Side effects | Internal redirect re-executes all phases | Zero — modifies status in header filter only |
| Conditional logic | None — matches exact status codes only | Supports if= and if!= with any variable |
| Performance | Re-enters rewrite, access, content phases | Single comparison in output filter chain |
The key difference: error_page performs an internal redirect. It clears all module contexts, re-initializes the location config, and re-executes the entire request pipeline. The upstream response body is lost.
The rewrite_status module operates as a header filter. It intercepts response headers after they leave the upstream but before NGINX sends them to the client. It changes the status in place and passes everything else through. The body, the upstream headers, the timing data — all preserved. This approach to NGINX rewrite response status code handling is clean and predictable.
How It Works
The module installs itself at the top of NGINX’s header filter chain. When a response arrives from upstream (or any content handler), the filter evaluates your rules in order:
- For each rule, if a condition is specified (
if=orif!=), the module evaluates the variable - If the condition is true (or absent), the status code is rewritten and evaluation stops
- If the condition is false, the module moves to the next rule
- After all rules are processed (or one matches), the response continues through remaining filters
This “first match wins” behavior lets you stack multiple rules. Only the first matching rule takes effect. The pattern resembles NGINX’s native map directive — except it operates on response status codes.
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-rewrite-status
Then load the module by adding the following at the very top of /etc/nginx/nginx.conf, before any other directives:
load_module modules/ngx_http_rewrite_status_filter_module.so;
Verify the module loads correctly:
nginx -t
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-rewrite-status
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
Module package page: nginx-module-rewrite-status on GetPageSpeed
Configuration Reference
rewrite_status
Syntax: rewrite_status <status_code> [if=<variable>]; or rewrite_status <status_code> [if!=<variable>];
Default: none
Context: http, server, location
The directive accepts a numeric HTTP status code (100–999) and an optional condition.
Parameters:
status_code— the HTTP status code to set on the response. Must be a literal integer. Variables are not supported here.if=variable— apply the rewrite only when the variable is non-empty and non-zero.if!=variable— apply the rewrite only when the variable is empty or equals"0".
You can place multiple rewrite_status directives in the same context. They are evaluated in order. The first matching rule wins.
Practical Use Cases
The following examples show common scenarios for NGINX rewrite response status code operations. Every config snippet and output below was tested on a live NGINX 1.28 instance.
Unconditional Status Rewrite
The simplest form rewrites every response to a fixed status code:
location /health {
rewrite_status 200;
proxy_pass http://backend;
}
Every response from /health returns 200, regardless of what the backend sends. This is useful for health check endpoints where the upstream may return non-200 codes.
Conditional Rewrite Based on a Variable
Use if= to apply the rewrite only when a variable is truthy:
location /api {
set $force_not_found "1";
rewrite_status 404 if=$force_not_found;
proxy_pass http://backend;
}
The variable can come from set, map, geo, upstream headers, or any other source.
Conditional Rewrite Using Request Headers
Client request headers work as conditions. NGINX exposes them as $http_<name>:
location /api {
rewrite_status 503 if=$http_x_force_maintenance;
proxy_pass http://backend;
}
When a client sends X-Force-Maintenance with any non-empty value, the response becomes 503. Without it, the upstream status passes through:
# Without the header — upstream status preserved
curl -s -o /dev/null -w "%{http_code}" http://localhost/api
# Output: 200
# With the header — status rewritten
curl -s -o /dev/null -w "%{http_code}" -H "X-Force-Maintenance: yes" http://localhost/api
# Output: 503
Conditional Rewrite Using Upstream Response Headers
The module runs in the header filter phase. This means $upstream_http_* variables are available:
location / {
rewrite_status 204 if=$upstream_http_x_no_content;
proxy_pass http://backend;
}
If the upstream sends an X-No-Content header, the status rewrites to 204. The upstream can signal overrides through headers without changing its own codes.
Negative Conditions with if!=
The if!= operator inverts the logic. The rewrite fires when the variable is empty or "0":
location /protected {
rewrite_status 403 if!=$http_authorization;
proxy_pass http://backend;
}
Omitting the Authorization header triggers a 403 response. Sending it lets the upstream status pass through. Note: this is a lightweight guard, not real authentication.
URL-Based Routing with map
Combine rewrite_status with map for pattern-based rewrites:
map $uri $is_maintenance {
~^/maintenance 1;
default 0;
}
map $uri $is_deprecated {
~^/deprecated 1;
default 0;
}
server {
listen 80;
location / {
rewrite_status 503 if=$is_maintenance;
rewrite_status 410 if=$is_deprecated;
proxy_pass http://backend;
}
}
Requests to /maintenance/* get 503. Requests to /deprecated/* get 410. Everything else keeps the upstream status. The response body is preserved:
curl -s -o /dev/null -w "%{http_code}" http://localhost:8082/normal
# Output: 200
curl -s -o /dev/null -w "%{http_code}" http://localhost:8082/maintenance/page
# Output: 503
curl -s -o /dev/null -w "%{http_code}" http://localhost:8082/deprecated/api/v1
# Output: 410
Multiple Rules — First Match Wins
When multiple directives are present, evaluation stops at the first match:
location /api {
set $is_admin "1";
set $is_beta "1";
rewrite_status 201 if=$is_admin;
rewrite_status 202 if=$is_beta;
proxy_pass http://backend;
}
The $is_admin variable is truthy, so the status becomes 201. The second rule is never evaluated.
Masking Upstream Error Codes
Some upstreams expose internal codes (502, 504) that leak infrastructure details. Normalize them with NGINX rewrite response status code rules:
map $upstream_status $mask_error {
502 1;
504 1;
default 0;
}
server {
listen 80;
location / {
rewrite_status 500 if=$mask_error;
proxy_pass http://backend;
}
}
Clients see 500 instead of 502 or 504, with the original upstream response body intact. This hides that the app sits behind a reverse proxy.
Converting Upstream 404 to 410 Gone
When soft-deleting resources, your API may return 404. Search engines need 410 to know the resource is gone permanently:
map $upstream_http_x_resource_deleted $is_deleted {
"true" 1;
default 0;
}
server {
listen 80;
location /api/resources {
rewrite_status 410 if=$is_deleted;
proxy_pass http://backend;
}
}
The backend application sets the X-Resource-Deleted: true header on responses for deleted resources. The module rewrites 404 to 410 while keeping the JSON body. Unlike error_page, the response body is not discarded.
Note: This relies on the upstream application sending custom headers on error responses. Most application frameworks (Django, Rails, Express, Spring) do this by default. If your upstream is another NGINX instance, use
add_header X-Resource-Deleted "true" always;— withoutalways, NGINX’sadd_headeronly attaches headers to successful (2xx/3xx) responses.
Response Body Preservation
The biggest advantage over error_page is that the upstream body survives the rewrite:
# Upstream returns 404 with body
curl -s http://backend/not-found
# Output: upstream says not found
# After rewrite_status 200, body preserved
curl -s http://localhost/rewritten-endpoint
# Output: upstream says not found
The only exception: rewrites to 204 or 304 strip the body per HTTP spec. This is correct behavior.
Performance Considerations
The module adds negligible overhead:
- Filter position: Runs as a header filter — the lightest NGINX filter type. No body processing.
- Evaluation cost: One variable lookup and string comparison per rule. Nanosecond-scale.
- No allocation: Modifies existing structures in place. No buffers or copies.
- No I/O: Unlike
error_page, it never touches the filesystem.
The error_page + proxy_intercept_errors alternative re-executes all request phases. That is orders of magnitude more expensive. For optimized NGINX proxy setups, the rewrite_status module is the lightweight choice.
Security Best Practices
Do Not Suppress Legitimate Errors
Blindly rewriting all errors to 200 is dangerous:
# ❌ Do NOT do this
location / {
rewrite_status 200;
proxy_pass http://backend;
}
This masks real failures. Monitoring, load balancers, and CDN health checks need accurate status codes.
Use Specific Conditions
Prefer targeted conditions over blanket rewrites:
# ✅ Good — rewrite only on specific condition
rewrite_status 503 if=$is_maintenance;
# ❌ Bad — unconditional rewrite affects everything
rewrite_status 503;
Avoid Masking Authentication Failures
Do not hide 401/403 responses from upstream auth systems. Clients need these codes for login dialogs and token refresh.
Audit Your Conditions
Variables in if= conditions should be carefully controlled. An attacker who can influence the variable (e.g., a crafted request header) could trigger unintended rewrites. Prefer map or geo variables over raw $http_* headers. Additionally, a request cookies filter module can sanitize request data upstream of your rewrite rules.
Troubleshooting
Module Not Loading
If nginx -t reports unknown directive "rewrite_status", verify:
- The module file exists:
ls /usr/lib64/nginx/modules/ngx_http_rewrite_status_filter_module.so - The
load_moduledirective is at the very top ofnginx.conf:load_module modules/ngx_http_rewrite_status_filter_module.so; user nginx; worker_processes auto;
Status Code Not Changing
If the directive is accepted but the status stays the same:
- Check your variable. Add
add_header X-Debug $your_variable always;to inspect it. - Verify filter-time availability. Variables like
$upstream_http_*are empty for local responses. - Check rule order. An earlier matching rule prevents later ones from firing.
Status Code Must Be a Literal
The status code argument rejects variables:
# ❌ Fails at config parse time
rewrite_status $dynamic_code if=$condition;
# ✅ Use a literal number
rewrite_status 503 if=$condition;
For dynamic codes, stack multiple directives:
rewrite_status 503 if=$is_maintenance;
rewrite_status 410 if=$is_deprecated;
rewrite_status 429 if=$is_rate_limited;
Body Stripped on 204 or 304
NGINX strips the body for these codes per HTTP spec. This is correct — not a bug.
Conclusion
The rewrite_status module fills a gap in NGINX’s native capabilities. It rewrites response status codes without discarding the body or triggering internal redirects. Its if= and if!= conditions work with any NGINX variable. This makes it ideal for maintenance pages, API normalization, error masking, and SEO changes.
The module is lightweight, runs in the header filter chain, and integrates with map, geo, and $upstream_http_* variables.
- Source code: ngx_http_rewrite_status_filter_module on GitHub
- RPM packages: GetPageSpeed RPM repository
