When working with Nginx as a reverse proxy, many developers encounter this frustrating behavior:
location ~ ^/foobar {
set $url http://example.com/something/index.php?var1=hello&access=$scheme://$host$uri;
proxy_pass $url;
}
The $uri
variable automatically URL-decodes special characters before the proxy_pass directive executes. For example:
- Original request:
/foobar/hello%20world
$uri
becomes:/foobar/hello world
- Result: HTTP 400 Bad Request
While $request_uri
preserves the original encoding, it fails when you need consistent path handling across rewritten URLs:
location ~ ^/indirect {
rewrite ^/indirect(.*) /foobar$1;
}
In this case, $request_uri
would still contain /indirect/...
rather than the rewritten /foobar/...
path.
Here's a robust approach that maintains URL encoding while handling path rewrites:
location ~ ^/indirect {
rewrite ^/indirect(.*) /foobar$1;
# Store the encoded version in a custom variable
set $encoded_uri $request_uri;
}
location ~ ^/foobar {
# Check if we have a stored encoded version
if ($encoded_uri) {
set $final_uri $encoded_uri;
}
# Otherwise use the current request URI
if ($final_uri = "") {
set $final_uri $request_uri;
}
# Clean up any remaining indirect references
set $final_uri "foobar${final_uri##*/foobar}";
set $url http://example.com/something/index.php?var1=hello&access=$scheme://$host$final_uri;
proxy_pass $url;
}
For more complex scenarios, the Nginx Lua module provides greater control:
location ~ ^/indirect {
rewrite ^/indirect(.*) /foobar$1;
access_by_lua_block {
ngx.var.encoded_uri = ngx.var.request_uri
}
}
location ~ ^/foobar {
access_by_lua_block {
local final_uri = ngx.var.encoded_uri or ngx.var.request_uri
final_uri = string.gsub(final_uri, "^.*/foobar", "/foobar")
ngx.var.final_url = "http://example.com/something/index.php?var1=hello&access="..
ngx.var.scheme.."://"..ngx.var.host..final_uri
}
proxy_pass $final_url;
}
Consider these additional improvements for production environments:
# Preserve original encoding during rewrites
map $request_uri $encoded_foobar_uri {
~^/indirect(/.*)$ "/foobar$1";
default $request_uri;
}
location / {
# Apply URI encoding only when needed
set_escape_uri $encoded_access $encoded_foobar_uri;
set $url http://example.com/something/index.php?var1=hello&access=$scheme://$host$encoded_access;
proxy_pass $url;
}
This solution maintains proper encoding while supporting both direct and indirect access patterns.
When working with Nginx as a reverse proxy, we often need to pass the request URI to backend servers as a parameter. The $uri
variable seems perfect for this, but it comes with a critical behavior: it's automatically URL-decoded by Nginx.
location ~ ^/foobar {
set $url http://example.com/something/index.php?var1=hello&access=$scheme://$host$uri;
proxy_pass $url;
}
This becomes problematic when handling URLs containing encoded characters. For example, accessing http://example.com/foobar/hello%20world
would result in $uri
containing /foobar/hello world
, which breaks the URL when used as a parameter value.
While $request_uri
maintains the original encoding, it doesn't work well with rewritten paths:
location ~ ^/indirect {
rewrite ^/indirect(.*) /foobar$1;
}
In this case, $request_uri
would still contain /indirect/...
even after rewrite, while we need the final path (/foobar/...
) in our backend parameter.
We can create a custom variable that combines the benefits of both approaches:
map $request_uri $encoded_uri {
default $request_uri;
"~*^/indirect(/.*)" "/foobar$1";
}
location / {
# ... other configuration ...
set $url http://example.com/something/index.php?var1=hello&access=$scheme://$host$encoded_uri;
proxy_pass $url;
}
This solution:
- Maintains original encoding for direct requests to /foobar
- Properly rewrites paths for indirect requests
- Preserves all URL-encoded characters
For more complex scenarios with multiple rewrite rules:
map $request_uri $encoded_uri {
default $request_uri;
"~*^/indirect1(/.*)" "/foobar$1";
"~*^/indirect2(/.*)" "/anotherpath$1";
"~*^/indirect3(/.*)" "/thirdpath$1";
}
This approach keeps all path transformation logic in one place, making it easier to maintain than scattering rewrites across multiple locations.
The map directive is evaluated during the configuration phase, so it adds negligible overhead to request processing. The solution scales well even with many rewrite patterns.