How to Handle URL-Decoded $uri in Nginx Reverse Proxy Without Breaking Request Encoding


2 views

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.