Introduction

Nginx location blocks use regex patterns to route requests, but a common pitfall is that query strings are not included in the URI that location blocks match against. When a regex location block seems to fail for URLs with query parameters, the issue is usually a misunderstanding of how Nginx processes request URIs. The URI passed to location matching is /path/to/resource, not /path/to/resource?key=value.

Symptoms

  • Regex location ~ /api/v1/.* works for /api/v1/users but not for /api/v1/users?expand=true
  • Request falls through to the default location / block instead of the intended regex block
  • Query string parameters appear to be ignored by rewrite rules
  • Confusing behavior where some paths match and others with the same path plus query string do not

Common Causes

  • Nginx location blocks match against $uri only, not $request_uri which includes the query string
  • Query strings contain characters that interfere with regex patterns (e.g., ?, &, =)
  • Percent-encoded characters in the URI (e.g., %20 for space) not matching the expected regex
  • Confusion between = (exact match), ~ (case-sensitive regex), and ~* (case-insensitive regex) modifiers

Step-by-Step Fix

  1. 1.Understand that location blocks do not see query strings. The URI /api/v1/users?expand=true is matched as /api/v1/users only. If you need to match based on query string presence, use an if block or map directive:
  2. 2.```nginx
  3. 3.server {
  4. 4.# This matches /api/v1/users regardless of query string
  5. 5.location ~ ^/api/v1/ {
  6. 6.proxy_pass http://backend;
  7. 7.}
  8. 8.}
  9. 9.`
  10. 10.Use map to route based on query string presence:
  11. 11.```nginx
  12. 12.map $arg_expand $backend_pool {
  13. 13.default app_pool;
  14. 14."true" expanded_pool;
  15. 15.}

server { location /api/v1/ { proxy_pass http://$backend_pool; } } `` The $arg_expand variable automatically extracts the expand` query parameter.

  1. 1.Use if for query-string-based rewrites (the only safe use of if in location context):
  2. 2.```nginx
  3. 3.location /api/v1/ {
  4. 4.if ($args ~ "format=csv") {
  5. 5.rewrite ^ /api/v1/export last;
  6. 6.}
  7. 7.proxy_pass http://backend;
  8. 8.}
  9. 9.`
  10. 10.Handle percent-encoded URIs properly. Use ~* for case-insensitive matching, which also helps with encoded characters:
  11. 11.```nginx
  12. 12.location ~* ^/files/[\w%-]+\.(pdf|docx?)$ {
  13. 13.alias /var/www/files/;
  14. 14.}
  15. 15.`
  16. 16.This matches /files/report%202024.pdf and /files/annual-report.pdf.
  17. 17.Debug matching with Nginx error log at debug level:
  18. 18.```nginx
  19. 19.# In nginx.conf, change error_log level
  20. 20.error_log /var/log/nginx/debug.log debug;
  21. 21.`
  22. 22.Then check which location block handles each request:
  23. 23.```bash
  24. 24.grep "test location" /var/log/nginx/debug.log
  25. 25.`

Prevention

  • Always test location regex patterns with curl -v to verify which block handles the request
  • Use map directives instead of if blocks when possible for query-string-based routing
  • Keep a test suite of URLs (with and without query strings) to validate location configurations after changes
  • Document which variables are available in which Nginx contexts ($uri vs $request_uri vs $args)
  • Avoid regex in location blocks when prefix matching suffices - prefix matching is faster and less error-prone