45

Can nginx location blocks match a URL query string?

For example, what location block might match HTTP GET request

GET /git/sample-repository/info/refs?service=git-receive-pack HTTP/1.1
Derek Mahar
  • 901
  • 3
  • 8
  • 16
  • I'd guess "location /git/sample-repository/info/refs?service=git-receive-pack" as nginx just does string comparison. – JosefScript Oct 28 '16 at 16:58
  • 1
    String comparison of the entire URL or just the part before the question mark (`?`)? – Derek Mahar Oct 28 '16 at 17:05
  • According to http://stackoverflow.com/questions/15713934/how-to-match-question-mark-as-regexp-on-nginx-conf-location, nginx considers the URI to be only the part before the question mark. Is this still the case? – Derek Mahar Oct 28 '16 at 17:07
  • You were right, I was wrong. In A location block "the matching is performed against a normalized URI". You could try it with a rewrite which uses the $request_uri ("full original request URI (with arguments)") – JosefScript Oct 28 '16 at 17:34
  • Where did you find this quotation? – Derek Mahar Oct 28 '16 at 17:37
  • 1
    http://nginx.org/en/docs/http/ngx_http_core_module.html#var_request_uri – JosefScript Oct 28 '16 at 17:39
  • 1
    http://nginx.org/en/docs/http/ngx_http_core_module.html#location – JosefScript Oct 28 '16 at 17:39
  • It's not clear from http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite that `rewrite` regular expression matches `$request_uri`. – Derek Mahar Oct 28 '16 at 18:25
  • "If the specified regular expression matches a request URI, URI is changed as specified in the replacement string." http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#rewrite – JosefScript Oct 28 '16 at 18:49
  • Let us [continue this discussion in chat](http://chat.stackexchange.com/rooms/47606/discussion-between-derek-mahar-and-josefscript). – Derek Mahar Oct 28 '16 at 18:53
  • 6
    Some last clarification as I stumbled over this problem myself: https://nginx.org/en/docs/http/request_processing.html clearly states: _"Note that locations of all types test only a URI part of request line without arguments. This is done because arguments in the query string may be given in several ways"_ – Thomas Urban Sep 29 '17 at 07:08

3 Answers3

60

Can nginx location blocks match a URL query string?

Short answer: No.

Long answer: There is a workaround if we have only a handful of such location blocks.

Here's a sample workaround for 3 location blocks that need to match specific query strings:

server {
  #... common definitions such as server, root
    
  location / {
    error_page 418 = @queryone;
    error_page 419 = @querytwo;
    error_page 420 = @querythree;

    if ( $query_string = "service=git-receive-pack" ) { return 418; }
    if ( $args ~ "service=git-upload-pack" ) { return 419; }
    if ( $arg_somerandomfield = "somerandomvaluetomatch" ) { return 420; }

    # do the remaining stuff
    # ex: try_files $uri =404;
    
  }

  location @queryone {
    # do stuff when queryone matches
  }

  location @querytwo {
    # do stuff when querytwo matches
  }

  location @querythree {
    # do stuff when querythree matches
  }
}

You may use $query_string, $args or $arg_fieldname. All will do the job. You may know more about error_page in the official docs.

Warning: Please be sure not to use the standard HTTP codes.

Pothi Kalimuthu
  • 6,117
  • 2
  • 26
  • 38
  • 1
    Interesting approach! May I recommend `$args ~ "service=git-send-pack"` instead of `$args = "service=git-send-pack"`? This form accommodates multiple query parameters. – Derek Mahar Oct 28 '16 at 21:52
  • `git-send-pack` should also read `git-upload-pack`. – Derek Mahar Oct 28 '16 at 21:52
  • Yes. +1 for tilde to match multiple queries! – Pothi Kalimuthu Oct 28 '16 at 21:54
  • 1
    http://stackoverflow.com/a/40313590/107158 illustrates the approach that I followed to handle query string arguments. Like your answer, mine uses `if` and `$arg_fieldname`, but uses `rewrite` instead of `error_page` and `location @name`. Note that in that example, my attempts at using `@name` for the *replacement* parameter in `rewrite` were unsuccessful. – Derek Mahar Oct 28 '16 at 21:57
  • 1
    By the way, it should be `$args ~` and `$arg_somerandomfield =`. – Derek Mahar Oct 28 '16 at 22:02
  • Yes. Good catch! Yes, both "return" and "rewrite ... last" work correctly inside an "if" statement as per the docs... https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/ . – Pothi Kalimuthu Oct 28 '16 at 22:05
  • 2
    One can also use nginx `map` feature for this purpose, which is faster. – Tero Kilkanen Oct 29 '16 at 10:24
  • http://www.redant.com.au/ruby-on-rails-devops/manage-ssl-redirection-in-nginx-using-maps-and-save-the-universe/ explains why and how to use `map`. – Derek Mahar Oct 31 '16 at 20:05
  • Why the standard HTTP code must not be used? – giorgiosironi Jun 13 '17 at 12:24
  • When we use standard HTTP code, Nginx will show the standard error page, rather than (internally) redirecting to the named location block. – Pothi Kalimuthu Jun 14 '17 at 04:47
  • +1 for 418 I'm a teapot – also this is a great solution – Steve Jun 23 '17 at 05:51
  • The `error_page` directive can be misleading. If a query parameter is set, that should not be considered as an error. I wonder if there is another `nginx` directive to be used instead of `error_page`. – W.M. Sep 09 '17 at 16:33
  • Good point @W.M. . The `error_page` usage to safely change the location was directly inspired by https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/#what-to-do-instead . Probably, there will be an alternative way to do this in the future, that doesn't mislead. – Pothi Kalimuthu Sep 11 '17 at 04:22
  • 1
    @PothiKalimuthu, thanks for clarifying this. What I have done in the meanwhile is to replace the `query` parameter by a url path one like this `feedback/{auth_key}` instead of `/feedback?auth_key=abc`. This way I don't need to use `if`, I can define location pattern using `regex` and that's it. – W.M. Sep 11 '17 at 16:26
  • note: `$query_string` or `$args` won't match a url like `/example/service=git-receive-pack`, even though once rewritten it has that query, in this case `$request_uri` seems to be the only option – ᴍᴇʜᴏᴠ Sep 06 '18 at 19:44
  • @PothiKalimuthu you look very experienced on Nginx, can u help me problem here https://serverfault.com/questions/1035805/how-to-serve-php-on-nginx-on-a-query-parameter-amp-1 – Faizan Sep 30 '20 at 06:48
9

I know this question is over a year old, but I've spent the last few days destroying my brain over a similar problem. I wanted different authentication and handling rules for public and private repos, including pushing and pulling. This is what I finally came up with, so I figured I'd share. I know if is a tricky directive, but this seems to work for me just fine:

# pattern for all repos, public or private, followed by username and reponame
location ~ ^(?:\/(private))?\/([A-Za-z0-9]+)\/([A-Za-z0-9]+)\.git(\/.*)?$ {

    # if this is a pull request
    if ( $arg_service = "git-upload-pack" ) {

        # rewrite url with a prefix
        rewrite ^ /upload$uri;

    }

    # if this is a push request
    if ( $arg_service = "git-receive-pack" ) {

        # rewrite url with a prefix
        rewrite ^ /receive$uri;

    }

}

# for pulling public repos
location ~ ^\/upload(\/([A-Za-z0-9]+)\/([A-Za-z0-9]+)\.git(\/.*)?)$ {

    # auth_basic "git";
    # ^ if you want

    # ...
    # fastcgi_pass unix:/var/run/fcgiwrap.socket;
    # ...

}

# for pushing public repos
location ~ ^\/receive(\/([A-Za-z0-9]+)\/([A-Za-z0-9]+)\.git(\/.*)?)$ {

    # auth_basic "git";
    # ^ if you want

    # ...
    # fastcgi_pass unix:/var/run/fcgiwrap.socket;
    # ...

}

# for pulling private repos
location ~ ^\/upload\/private(\/([A-Za-z0-9]+)\/([A-Za-z0-9]+)\.git(\/.*)?)$ {

    # auth_basic "git";
    # ^ if you want

    # ...
    # fastcgi_pass unix:/var/run/fcgiwrap.socket;
    # ...

}

# for pushing private repos
location ~ ^\/receive\/private(\/([A-Za-z0-9]+)\/([A-Za-z0-9]+)\.git(\/.*)?)$ {

    # auth_basic "git";
    # ^ if you want

    # ...
    # fastcgi_pass unix:/var/run/fcgiwrap.socket;
    # ...

}
Jared Brandt
  • 391
  • 2
  • 7
  • 1
    As NGINX uses PCRE library, I would suggest to use `\w` instead of `[A-Za-z0-9_]` to diminish verbosity. – Stphane Nov 29 '20 at 22:41
-1

There is another way this can be done, if you are using nginx as a proxy.

set a variable in the server block:

set $nocache="0";

Inside the location block, add the if:

if ( $arg_<query string to match> = "<query string value>") { set $nocache "1"; }

And add two new proxy directives:

proxy_cache_bypass $nocache ;
proxy_no_cache $nocache ;

it will always forward to the upstream server with no cache