How to Route Nginx Reverse Proxy Based on Cookie Values (Equivalent to Apache mod_rewrite Behavior)


6 views

When migrating from Apache to Nginx, one common pain point is replicating cookie-based routing behavior. In Apache, we could easily use mod_rewrite with RewriteCond to check cookie values and route requests accordingly. However, Nginx requires a different approach.

The first approach using $cookie_proxy_override often fails because:

location /original-request {
    if ($cookie_proxy_override = "proxy-target-A") {
        # This may not work as expected
        rewrite . http://backend-a/some-application;
        break;
    }
    # ... other conditions
}

The rewrite directive with an absolute URL causes a 302 redirect rather than internal proxy routing. This is fundamentally different behavior from Apache's [P] flag.

Here's the correct way to implement cookie-based routing in Nginx:

map $cookie_proxy_override $backend {
    default "http://primary-backend/original-application";
    "proxy-target-A" "http://backend-a/some-application";
    "proxy-target-B" "http://backend-b/another-application";
}

server {
    # ... other server config
    
    location /original-request {
        proxy_pass $backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

1. The map directive provides clean, maintainable routing logic
2. No conditional blocks that can cause unexpected behavior
3. Pure proxy behavior without unwanted redirects
4. All proxy-related headers are properly preserved

For more complex cookie matching (regex, partial matches), use:

map $http_cookie $backend {
    default "http://primary-backend/original-application";
    ~*proxy-target-A "http://backend-a/some-application";
    ~*proxy-target-B "http://backend-b/another-application";
}

1. Verify cookie values reach Nginx with:
add_header X-Debug-Cookie $cookie_proxy_override;
2. Check which backend was selected:
add_header X-Backend-Selected $backend;
3. Use curl to test:
curl -v --cookie "proxy_override=proxy-target-A" http://yourserver/original-request

The map directive is evaluated during the rewrite phase, making it very efficient. For high-traffic sites, consider:

map_hash_bucket_size 128;  # For longer cookie values
map_hash_max_size 2048;    # For many routing rules

For extremely dynamic routing needs, OpenResty's Lua integration provides more flexibility:

location /original-request {
    access_by_lua_block {
        local cookie = ngx.var.cookie_proxy_override
        if cookie == "proxy-target-A" then
            ngx.var.backend = "http://backend-a/some-application"
        elseif cookie == "proxy-target-B" then
            ngx.var.backend = "http://backend-b/another-application"
        end
    }
    proxy_pass $backend;
}

When transitioning from Apache to Nginx, one of the trickiest parts is converting complex rewrite rules that rely on HTTP cookies for backend routing. The original Apache setup worked perfectly:

RewriteCond %{HTTP_COOKIE}  proxy-target-A
RewriteRule ^/original-request/ http://backend-a/some-application [P,QSA]

RewriteCond %{HTTP_COOKIE}  proxy-target-B
RewriteRule ^/original-request http://backend-b/another-application [P,QSA]

RewriteRule ^/original-request http://primary-backend/original-application [P,QSA]

The first Nginx attempt using $cookie_proxy_override didn't work as expected. While Nginx could read the cookie value, the conditional blocks never triggered. Here's what didn't work:

location /original-request {
    if ($cookie_proxy_override = "proxy-target-A") {
        rewrite . http://backend-a/some-application;
        break;
    }
    if ($cookie_proxy_override = "proxy-target-B") {
        rewrite . http://backend-b/another-application;
        break;
    }
    proxy_pass http://primary-backend/original-application;
}

Switching to $http_cookie with regex matching solved the condition triggering, but introduced a new issue - unwanted 302 redirects:

location /original-request {
    if ($http_cookie ~ "proxy-target-A") {
        rewrite . http://backend-a/some-application;
        break;
    }
    if ($http_cookie ~ "proxy-target-B") {
        rewrite . http://backend-b/another-application;
        break;
    }
    proxy_pass http://primary-backend/original-application;
}

The key realization was that rewrite causes redirects, while we want internal proxying. Here's the working configuration:

location /original-request {
    set $backend "http://primary-backend/original-application";
    
    if ($http_cookie ~ "proxy-target-A") {
        set $backend "http://backend-a/some-application";
    }
    
    if ($http_cookie ~ "proxy-target-B") {
        set $backend "http://backend-b/another-application";
    }
    
    proxy_pass $backend;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

For more complex scenarios, consider these enhancements:

map $cookie_proxy_override $backend {
    "proxy-target-A" "http://backend-a/some-application";
    "proxy-target-B" "http://backend-b/another-application";
    default          "http://primary-backend/original-application";
}

server {
    location /original-request {
        proxy_pass $backend;
        # Additional proxy settings...
    }
}

To verify your configuration:

# Check cookie value extraction
add_header X-Debug-Cookie $cookie_proxy_override always;

# Log backend selection
log_format backend_log '$remote_addr - $cookie_proxy_override - "$request" - $backend';
access_log /var/log/nginx/backend.log backend_log;

Remember that Nginx processes if directives with some limitations. The map directive often provides cleaner solutions for complex routing logic.