Site icon GetPageSpeed

NGINX Internal Redirect Module: Install and Configure

NGINX Internal Redirect Module: Route Requests Without Rewrite Phase Limits

The NGINX internal redirect module enables regex-based URI routing in request phases that come after the built-in rewrite directive. While rewrite runs exclusively in the rewrite phase — one of the earliest stages of request processing — the NGINX internal redirect module operates in the preaccess, access, precontent, or content phases. This means it can act on variables set by modules like map, geo, and authentication handlers that are simply unavailable during the rewrite phase.

If you manage NGINX servers and need routing decisions based on runtime conditions, such as authentication results or GeoIP data, the NGINX internal redirect module provides a clean, declarative syntax for phase-aware request routing without resorting to the problematic if directive.

How the Internal Redirect Module Works

When NGINX processes a request, it passes through a series of phases in a fixed order:

  1. Server rewrite phaserewrite directives in server context
  2. Find config phase — location matching
  3. Rewrite phaserewrite and return directives in location context
  4. Post-rewrite phase — internal redirect after URI change
  5. Preaccess phase — rate limiting, connection limits
  6. Access phase — IP-based access control, authentication
  7. Precontent phasetry_files processing
  8. Content phase — response generation (proxy_pass, fastcgi_pass, return)

The built-in rewrite directive operates in phase 3. The NGINX internal redirect module, however, registers handlers in phases 5 through 8. This means it can evaluate conditions and redirect requests after authentication, rate limiting, and access control have already set their variables.

After performing an internal redirect, the request returns to the server rewrite phase and proceeds through location matching again with the new URI. This is the same mechanism that try_files and the index module use internally.

Why Not Just Use rewrite?

The rewrite directive handles most URI transformations perfectly well. However, there are scenarios where the NGINX internal redirect module is the better choice:

Additionally, NGINX Plus offers its own commercial internal_redirect directive (since version 1.23.4), but it only accepts a simple URI — no regex matching, no phase selection, and no conditional logic. The open-source NGINX internal redirect module documented here is significantly more powerful.

Installation

RHEL, CentOS, AlmaLinux, Rocky Linux

Install the GetPageSpeed repository and the module package:

sudo dnf install https://extras.getpagespeed.com/release-latest.rpm
sudo dnf install nginx-module-internal-redirect

Then load the module by adding the following line to the top of /etc/nginx/nginx.conf, before any http block:

load_module modules/ngx_http_internal_redirect_module.so;

For more details and available versions, see the RPM module page.

Debian and Ubuntu

First, set up the GetPageSpeed APT repository, then install:

sudo apt-get update
sudo apt-get install nginx-module-internal-redirect

On Debian/Ubuntu, the package handles module loading automatically. No load_module directive is needed.

The internal_redirect Directive

Syntax:

internal_redirect [-i] pattern replacement [phase=<phase>] [flag=<flag>] [if=<condition> | if!=<condition>]

Default: none

Context: server, location

The directive takes a PCRE regular expression pattern and a replacement URI. When the pattern matches the current request URI, NGINX performs an internal redirect to the replacement URI. The replacement can contain regex capture group references ($1, $2, etc.) and NGINX variables.

Parameters

Parameter Description
-i Perform case-insensitive regex matching
pattern PCRE regular expression to match against the request URI
replacement Target URI for the internal redirect. Supports capture groups and variables. Must start with / or @
phase= Request phase in which the redirect runs: preaccess (default), access, precontent, or content
flag= Additional action: break, status_301, status_302, status_303, status_307, or status_308
if= Only redirect when the condition evaluates to a non-empty, non-zero value
if!= Only redirect when the condition evaluates to empty or zero (negated condition)

Phase Selection

The phase= parameter controls when the redirect rule is evaluated during request processing:

Flag Options

The flag= parameter modifies behavior after a successful match:

Conditional Evaluation

The if= and if!= parameters accept any NGINX variable or complex expression. The condition is considered false when it evaluates to an empty string or the string "0". Everything else is considered true.

Practical Examples

Basic URI Migration

Redirect requests from an old URI structure to a new one, transparently:

server {
    listen 80;
    server_name example.com;

    location /old-blog {
        internal_redirect ^/old-blog/(.+)$ /articles/$1 phase=preaccess;
    }

    location /articles {
        proxy_pass http://backend;
    }
}

When a client requests /old-blog/my-post, NGINX internally redirects to /articles/my-post and serves the response from the /articles location. The client’s browser URL does not change.

Case-Insensitive Matching

Handle URIs where casing is unpredictable, such as links shared on social media. Note that the -i flag only affects the internal_redirect regex — you must also use a case-insensitive regex location (~*) to ensure NGINX routes all casing variants into the same location block:

location ~* ^/products {
    internal_redirect -i ^/products/(.+)$ /shop/$1 phase=preaccess;
}

location /shop {
    proxy_pass http://catalog-backend;
}

The ~* location matches /Products/Widget, /PRODUCTS/widget, and /products/Widget equally. The -i flag ensures the internal_redirect regex also matches regardless of case.

Named Location Redirect

Route requests to a named location for specialized processing:

location /api {
    internal_redirect ^/api/v1/(.+)$ @api_handler phase=preaccess;
}

location @api_handler {
    internal;
    proxy_pass http://api-backend;
}

Named locations (prefixed with @) are only accessible via internal redirects, providing an extra layer of isolation.

Conditional Redirect Based on a map Variable

Use map to set a routing variable, then act on it with internal_redirect:

map $uri $route_target {
    default "";
    ~^/promo/.+ /campaigns;
}

server {
    listen 80;
    server_name example.com;

    location /promo {
        internal_redirect ^/promo/(.+)$ $route_target/$1 phase=preaccess if=$route_target;
    }

    location /campaigns {
        proxy_pass http://campaigns-backend;
    }
}

The redirect only fires when $route_target is non-empty. If the map does not match, the condition evaluates to false and the request continues normally.

Multiple Rules with break Flag

Process multiple redirect rules and stop at the first match using flag=break:

location / {
    # Check rules in order; stop at first match
    internal_redirect ^/legacy/admin(.*)$ /admin-panel$1 phase=preaccess flag=break;
    internal_redirect ^/legacy/api(.*)$   /api/v2$1     phase=preaccess flag=break;
    internal_redirect ^/legacy/(.*)$      /modern/$1    phase=preaccess;
}

Without flag=break, all matching rules in the same phase execute sequentially, and the last match determines the final redirect URI. With break, processing stops immediately after the first successful match.

External Redirect with Status Code

Use flag=status_301 or flag=status_302 to send a client-visible redirect instead of an internal one:

location /old-site {
    internal_redirect ^/old-site/(.*)$ https://new.example.com/$1
        phase=preaccess flag=status_301;
}

This sends an HTTP 301 Moved Permanently response with the Location header set to https://new.example.com/...`. The client's browser follows the redirect. This is functionally similar torewrite … permanent` but runs in the preaccess phase.

Negated Condition with if!=

Redirect all requests except when a specific condition is true:

map $remote_addr $is_internal {
    default "0";
    ~^10\..+  "1";
    ~^192\.168\..+ "1";
}

server {
    listen 80;
    server_name example.com;

    location /admin {
        # Redirect non-internal users to the login page
        internal_redirect ^/admin(.*)$ /login phase=preaccess if!=$is_internal;
        proxy_pass http://admin-backend;
    }

    location /login {
        proxy_pass http://auth-backend;
    }
}

When $is_internal is "0" (non-internal IP), the if!= condition is true and the redirect fires. Internal users proceed directly to the admin backend.

Query String Handling

The NGINX internal redirect module matches the regex pattern against the full URI including query string arguments. When the request has query parameters, the module constructs the match string as $uri?$args.

For example, a request to /search?q=nginx&page=2 is matched against the string /search?q=nginx&page=2. Therefore, if you want to match a URI that may have query parameters, account for the ? in your regex:

# Match /search with or without query string
location /search {
    internal_redirect "^/search(\?.+)?$" /new-search$1 phase=preaccess;
}

When the replacement URI contains a query string, it is parsed and set as the new $args. If you redirect to a URI without a query string, the original arguments are discarded.

Important: Phase Ordering with return

A common mistake is placing a return directive in the same location as internal_redirect. The return directive executes during the rewrite phase, which comes before any of the phases where internal_redirect operates. Therefore, return always takes precedence:

# WRONG: return always wins because it runs in the rewrite phase
location /example {
    internal_redirect ^/example$ /target phase=preaccess;
    return 200 "This always runs\n";
}

If you need a fallback response when the redirect does not fire, use try_files or proxy_pass instead of return:

# CORRECT: use a content-phase handler as fallback
location /example {
    internal_redirect ^/example$ /target phase=preaccess if=$should_redirect;
    try_files $uri =404;
}

This behavior is consistent with how NGINX request phases work — earlier phases always execute before later ones, regardless of directive order in the configuration file.

Infinite Loop Protection

NGINX has built-in protection against infinite redirect loops. Each request is limited to 10 internal redirects (controlled by the uri_changes counter in the NGINX core). If the limit is exceeded, NGINX returns a 500 Internal Server Error and logs a message:

rewrite or internal redirection cycle

Additionally, the NGINX internal redirect module avoids unnecessary redirects when the replacement URI matches the current URI. If the regex matches but the replacement evaluates to the same URI, the module updates only the query string arguments (if different) and allows the request to continue without restarting the phase cycle.

Troubleshooting

“unknown directive” Error

If nginx -t reports unknown directive "internal_redirect", the module is not loaded. Verify the load_module line is present at the top of nginx.conf:

load_module modules/ngx_http_internal_redirect_module.so;

Also verify the module file exists:

ls /usr/lib64/nginx/modules/ngx_http_internal_redirect_module.so

Redirect Not Firing

If the redirect does not seem to take effect:

  1. Check for return or rewrite ... last in the same location — these run before internal_redirect and may short-circuit the request
  2. Verify the regex pattern — use nginx -t to confirm there are no syntax errors. Test your regex separately with a tool like pcre2grep
  3. Check the if= condition — add a temporary add_header X-Debug-Var $your_variable always; to verify the variable’s value at request time
  4. Check phase ordering — if you use phase=content and another content handler like proxy_pass is also present, they may conflict

Unexpected 500 Errors

A 500 Internal Server Error with the message rewrite or internal redirection cycle in the error log means the redirect is looping. Common causes:

Resolve this by ensuring the replacement URI routes to a different location or by adding a condition to prevent re-matching.

Performance Considerations

The NGINX internal redirect module adds minimal overhead. Each internal_redirect directive compiles its regex pattern at configuration load time, so the runtime cost is limited to regex matching during request processing. For most configurations, this is negligible compared to network I/O and backend processing time.

However, each successful internal redirect causes NGINX to restart request processing from the server rewrite phase. This means all phase handlers run again for the new URI. Avoid chaining multiple internal redirects when a single redirect can achieve the same result.

Conclusion

The NGINX internal redirect module fills an important gap in NGINX’s request processing model. By operating in later request phases, it enables routing decisions that the built-in rewrite directive simply cannot make. Whether you need to redirect based on authentication results, map variables, or GeoIP data, this module provides a clean, declarative syntax for phase-aware request routing.

The module source code is available on GitHub. For pre-built packages, visit the GetPageSpeed module page.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Exit mobile version