yum upgrades for production use, this is the repository for you.
Active subscription is required.
The NGINX substitutions filter module is the most powerful tool for modifying content served through NGINX. Whether you’re rewriting URLs in HTML pages, replacing domain names during migrations, or stripping tracking codes from proxied responses—the nginx substitutions filter module (officially ngx_http_substitutions_filter_module) handles it all.
This NGINX substitutions filter module scans NGINX response bodies and performs both fixed-string and regular expression substitutions, line by line. Unlike the built-in ngx_http_sub_module which only supports a single substitution rule per location, this module allows multiple rules, regex patterns, and case-insensitive matching.
What is the nginx substitutions filter module?
The NGINX substitutions filter module is a third-party NGINX module that modifies HTTP response bodies by replacing text strings or patterns with replacement values. As stated in the official documentation, it “scans the output chains buffer and matches string line by line, just like Apache’s mod_substitute.”
The module operates as an output filter in NGINX’s processing pipeline, meaning it intercepts the response after it’s generated (by upstream servers, static files, or other handlers) but before it’s sent to the client. This makes it perfect for transforming content from legacy backends without modifying the backend code.
Key capabilities
- Multiple substitution rules per location (unlike native
sub_filter) - Regular expression matching with PCRE for complex patterns
- Case-insensitive matching for both fixed strings and regex
- NGINX variable support in replacement strings (e.g.,
$host,$request_uri) - Conditional bypass to disable filtering based on variables
- MIME type filtering to apply substitutions only to specific content types
NGINX substitutions filter vs. native sub_filter
NGINX includes a built-in ngx_http_sub_module with the sub_filter directive. Here’s how the nginx substitutions filter module compares:
| Feature | sub_filter (native) | subs_filter (this module) |
|---|---|---|
| Multiple rules | Only one string replacement | Unlimited rules per location |
| Regex support | No | Yes (PCRE) |
| Case-insensitive | sub_filter_once off; workaround |
Native i flag |
| Multiple matches | sub_filter_once off; |
Default behavior |
| Content types | sub_filter_types |
subs_filter_types |
| Variables in replacement | Yes | Yes |
| Variables in match pattern | Yes | Fixed strings only (not regex) |
When to use the nginx substitutions filter module instead of native sub_filter:
– You need multiple distinct substitutions in the same location
– You require regex patterns (e.g., matching version numbers, dynamic URLs)
– You want case-insensitive matching without regex complexity
Installation
RHEL, CentOS, AlmaLinux, Rocky Linux
Install from the GetPageSpeed repository:
sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-substitutions
Then load the module in /etc/nginx/nginx.conf:
load_module modules/ngx_http_subs_filter_module.so;
For detailed package information, see the nginx-module-substitutions RPM page.
Debian and Ubuntu
First, set up the GetPageSpeed APT repository, then install:
sudo apt-get update
sudo apt-get install nginx-module-substitutions
On Debian/Ubuntu, the package handles module loading automatically. No
load_moduledirective is needed.
For detailed package information, see the nginx-module-substitutions APT page.
Configuration Directives
The nginx substitutions filter module provides five directives:
subs_filter
Syntax: subs_filter pattern replacement [flags]
Default: none
Context: http, server, location
The main directive for defining substitution rules.
# Fixed string replacement (default)
subs_filter "old-domain.com" "new-domain.com";
# Case-insensitive fixed string
subs_filter "HTTP://" "https://" i;
# Regex replacement
subs_filter "v\d+\.\d+\.\d+" "v2.0.0" r;
# Regex with capture groups
subs_filter "(cdn\d+)\.example\.com" "$1.newcdn.com" ir;
# Replace only first occurrence
subs_filter "error" "warning" o;
Available flags:
– g — Replace all occurrences (default behavior)
– i — Case-insensitive matching
– o — Replace only the first occurrence (“once”)
– r — Treat pattern as a regular expression
Using NGINX variables in replacements:
# Insert the current host
subs_filter "HOSTNAME_PLACEHOLDER" "$host";
# Dynamic replacement based on request
subs_filter "api-endpoint" "https://$host/api/v2";
subs_filter_types
Syntax: subs_filter_types mime-type [mime-types ...]
Default: subs_filter_types text/html
Context: http, server, location
Specifies which content types should be processed for substitutions.
# Process HTML, CSS, JavaScript, and XML
subs_filter_types text/html text/css text/javascript application/xml;
# Process JSON API responses
subs_filter_types application/json;
# Wildcard for all text types
subs_filter_types text/html text/plain text/css text/xml text/javascript application/json application/xml;
Note:
text/htmlis always included by default. Specifying it again generates a harmless warning about duplicate MIME types.
subs_filter_bypass
Syntax: subs_filter_bypass $variable1 [$variable2 ...]
Default: none
Context: http, server, location
Disables substitution filtering when any specified variable is non-empty and not equal to “0”.
location / {
set $bypass 0;
# Bypass for authenticated admin users
if ($cookie_admin_bypass) {
set $bypass 1;
}
# Bypass for specific user agents
if ($http_user_agent ~* "monitoring-bot") {
set $bypass 1;
}
subs_filter_bypass $bypass;
subs_filter "internal-link" "public-link";
}
This is useful for:
– Allowing certain users to see unmodified content
– Bypassing substitution for monitoring or testing
– Performance optimization when substitution isn’t needed
subs_buffers
Syntax: subs_buffers number size
Default: subs_buffers 32 4k (approximately 128KB total)
Context: http, server, location
Configures the number and size of buffers used for storing output. This is similar to how proxy_buffer_size works for upstream responses.
# For responses up to 256KB
subs_buffers 32 8k;
# For larger responses up to 1MB
subs_buffers 64 16k;
The total buffer capacity equals number × size. Responses larger than this may cause issues or incomplete substitutions.
subs_line_buffer_size
Syntax: subs_line_buffer_size number size
Default: 8 × pagesize (typically 32KB)
Context: http, server, location
Configures the line buffer used during pattern matching.
# Increase for pages with very long lines
subs_line_buffer_size 8 64k;
Important: Due to a bug in current versions, this directive incorrectly requires two arguments. The first argument is used as the buffer size; the second is ignored. For example,
subs_line_buffer_size 64k 0sets the buffer to 64KB.
Practical Examples
Domain migration
Replace all references to the old domain with the new one:
server {
listen 80;
server_name new-domain.com;
location / {
proxy_pass http://legacy-backend;
proxy_set_header Accept-Encoding "";
subs_filter_types text/html text/css text/javascript;
subs_filter "old-domain.com" "new-domain.com" gi;
subs_filter "//old-domain.com" "//new-domain.com" g;
subs_filter "https://old-domain.com" "https://new-domain.com" g;
}
}
Force HTTPS links
Convert HTTP links to HTTPS in proxied content. This pairs well with other security modules for a complete security setup:
location / {
proxy_pass http://upstream;
proxy_set_header Accept-Encoding "";
subs_filter "http://$host" "https://$host" g;
subs_filter 'src="http:' 'src="https:' g;
subs_filter "href='http:" "href='https:" g;
}
Inject tracking code
Add analytics script before the closing body tag:
location / {
proxy_pass http://backend;
proxy_set_header Accept-Encoding "";
subs_filter "</body>" "<script src='/analytics.js'></script></body>" i;
}
Remove unwanted content
Strip advertisements or tracking pixels:
location / {
proxy_pass http://upstream;
proxy_set_header Accept-Encoding "";
# Remove Google Analytics (regex mode)
subs_filter "<script[^>]*google-analytics[^<]*</script>" "" r;
# Remove specific div by class
subs_filter '<div class="ads">[^<]*</div>' "" r;
}
Conditional branding
Apply different substitutions based on the hostname using NGINX’s map directive:
map $host $brand_name {
default "Default Brand";
brand1.com "Brand One";
brand2.com "Brand Two";
}
server {
location / {
proxy_pass http://backend;
proxy_set_header Accept-Encoding "";
subs_filter "BRAND_NAME" "$brand_name";
}
}
Important Caveats and Limitations
Line-by-line processing
The module processes content line by line, scanning each line terminated by a newline character (\n). This architectural decision has important implications:
- Cannot match across line boundaries: A pattern like
</div>\s*<div>won’t match if there’s a newline between the tags - Very long lines require buffer tuning: If your content has lines longer than the default buffer, increase
subs_line_buffer_size - Minified content works well: Single-line minified HTML/CSS/JS is processed efficiently
Server-level rules ignored with location rules
Critical caveat: When a location block defines its own subs_filter directives, all server-level subs_filter directives are completely ignored for that location.
server {
# This rule applies to all locations WITHOUT their own subs_filter
subs_filter "company" "ACME Corp";
location /api {
# Server-level rule is IGNORED here because this location has its own rule
subs_filter "version" "2.0";
# "company" will NOT be replaced in /api responses!
}
location /docs {
# Server-level rule applies here (no location-level subs_filter)
}
}
Workaround: Duplicate server-level rules in each location that needs its own rules:
location /api {
subs_filter "company" "ACME Corp"; # Repeated from server level
subs_filter "version" "2.0";
}
Dollar sign in regex patterns
The dollar sign ($) cannot be used in regex patterns because NGINX interprets it as a variable marker before the module processes the pattern.
# FAILS with "match part cannot contain variable during regex mode"
subs_filter "nginx$" "replaced" r;
subs_filter "line-end\$" "replaced" r; # Escaping doesn't help
Workaround: If you need to match end-of-string, redesign your pattern to match specific trailing content instead:
# Instead of matching "text$", match "text" followed by expected characters
subs_filter "text</div>" "replaced</div>" r;
Compressed responses are skipped
The nginx substitutions filter module cannot process compressed (gzip, brotli, deflate) response bodies. If the upstream server sends a compressed response, you’ll see this warning in the error log:
http subs filter header ignored, this may be a compressed response.
Solution: Prevent upstream from sending compressed responses:
location / {
proxy_pass http://upstream;
proxy_set_header Accept-Encoding ""; # Request uncompressed response
subs_filter "pattern" "replacement";
}
The module is compatible with NGINX’s gzip directive—it processes uncompressed content, then NGINX can compress the final response to the client. Learn more about NGINX Brotli compression.
Cannot substitute response headers
The module only modifies response bodies, not headers. To modify headers, use the headers-more module:
# NGINX's native header modification
proxy_hide_header X-Old-Header;
add_header X-New-Header "value";
# Or the headers-more module for more control
more_set_headers "X-Custom: value";
HTTP/1.0 response handling
When NGINX receives an HTTP/1.0 request and substitution is applied, the response may lack proper Content-Length or Transfer-Encoding headers, potentially causing issues with some clients. Modern clients using HTTP/1.1 or HTTP/2 are not affected.
Performance Considerations
Number of rules
Each subs_filter directive adds processing overhead. For fixed-string matching, the module uses an efficient memory search. For regex patterns, PCRE is invoked for each line.
Guidelines:
– Fixed-string rules: 10-50 rules typically have negligible impact
– Regex rules: Keep to essential patterns; each regex is compiled and executed per line
– 1000+ rules: Consider application-level processing or specialized tools
Buffer sizing
The default buffers (128KB total) work for most responses. For large responses:
# 512KB total buffer (64 × 8KB)
subs_buffers 64 8k;
Undersized buffers can cause truncated responses or errors.
Response size
Since the module removes Content-Length and switches to chunked transfer encoding (for HTTP/1.1), there’s minimal overhead from varying response sizes after substitution.
Troubleshooting
Substitution not happening
- Check content type: Ensure the response MIME type is in
subs_filter_types - Check compression: Verify upstream isn’t sending compressed content
- Check bypass conditions: Review
subs_filter_bypassvariables - Check location inheritance: If location has its own rules, server rules don’t apply
Enable debug logging
error_log /var/log/nginx/error.log debug;
Look for messages containing “subs” to see the module’s activity.
Verify module is loaded
# Check if the dynamic module file exists
ls /usr/lib64/nginx/modules/ngx_http_subs_filter_module.so
# Or verify it's loaded in your configuration
nginx -T 2>&1 | grep -i subs_filter
# List available directives in the module
strings /usr/lib64/nginx/modules/ngx_http_subs_filter_module.so | grep subs_
Conclusion
The nginx substitutions filter module provides powerful response body transformation capabilities that go far beyond NGINX’s built-in sub_filter. With support for multiple rules, regex patterns, and conditional bypass, it handles domain migrations, content injection, link rewriting, and content sanitization with ease.
Key points to remember:
– Always disable upstream compression with proxy_set_header Accept-Encoding ""
– Server-level rules don’t inherit to locations with their own rules
– Line-by-line processing means no cross-line pattern matching
– Dollar signs can’t be used in regex patterns
The nginx substitutions filter module is actively maintained and available through GetPageSpeed repositories for RHEL-based and Debian-based distributions.
