How nginx's "location if" works (Was Re: Setting cache parameters via if directives)

António P. P. Almeida appa at perusio.net
Tue Feb 15 01:22:25 MSK 2011


On 14 Fev 2011 05h27 WET, agentzh at gmail.com wrote:

> Well, ngx_proxy module's directive inheritance is in action here,
> which gives you nice side effects that you want :)
>
> I'll analyze some examples here such that people *may* get some
> light.
>
> [Case 1]
>
> location /proxy {
> set $a 32;
> if ($a = 32) {
> set $a 56;
> }
> set $a 76;
> proxy_pass http://127.0.0.1:$server_port/$a;
> }
>
> location ~ /(\d+) {
> echo $1;
> }
>
> Calling /proxy gives 76 because it works in the following steps:
>
> 1. Nginx runs all the rewrite phase directives in the order that
> they're in the config file, i.e.,
>
> set $a 32;
> if ($a = 32) {
> set $a 56;
> }
> set $a 76;
>
> and $a gets the final value of 76.
>
> 2. Nginx traps into the "if" inner block because its condition $a =
>    32
> was met in step 1.
>
> 3. The inner block does not has any content handler, ngx_proxy
> inherits the content handler (that of ngx_proxy) in the outer scope
> (see src/http/modules/ngx_http_proxy_module.c:2025).
>
> 4. Also the config specified by proxy_pass also gets inherited by
>    the
> inner "if" block (see src/http/modules/ngx_http_proxy_module.c:2015)
>
> 5. Request terminates (and the control flow never goes outside of
>    the
> "if" block).
>
> That is, the proxy_pass directive in the outer scope will never run
> in this example. It is "if" inner block that actually serves you.
>
> Let's see what happens when we override the inner "if" block's
> content handler with out own:
>
> [Case 2]
>
> location /proxy {
> set $a 32;
> if ($a = 32) {
> set $a 56;
> echo "a = $a";
> }
> set $a 76;
> proxy_pass http://127.0.0.1:$server_port/$a;
> }
> location ~ /(\d+) {
> echo $1;
> }
>
> You will get this while accessing /proxy:
>
> a = 76
>
> Looks counter-intuitive? Oh, well, let's see what's happening this
> time:
>
> 1. Nginx runs all the rewrite phase directives in the order that
> they're in the config file, i.e.,
>
> set $a 32;
> if ($a = 32) {
> set $a 56;
> }
> set $a 76;
>
> and $a gets the final value of 76.
>
> 2. Nginx traps into the "if" inner block because its condition $a =
>    32
> was met in step 1.
>
> 3. The inner block *does* has a content handler specified by "echo",
> then the value of $a (76) gets emitted to the client side.
>
> 4. Request terminates (and the control flow never goes outside of
>    the
> "if" block), as in Case 1.
>
> We do have a choice to make Case 2 work as we like:
>
> [Case 3]
>
> location /proxy {
> set $a 32;
> if ($a = 32) {
> set $a 56;
> break;
>
> echo "a = $a";
> }
> set $a 76;
> proxy_pass http://127.0.0.1:$server_port/$a;
> }
> location ~ /(\d+) {
> echo $1;
> }
>
> This time, we just add a "break" directive inside the if block. This
> will stop nginx from running the rest ngx_rewrite directives. So we
> get
>
> a = 56
>
> So this time, nginx works this way:
>
> 1. Nginx runs all the rewrite phase directives in the order that
> they're in the config file, i.e.,
>
> set $a 32;
> if ($a = 32) {
> set $a 56;
> break;
> }
>
> and $a gets the final value of 56.
>
> 2. Nginx traps into the "if" inner block because its condition $a =
>    32
> was met in step 1.
>
> 3. The inner block *does* has a content handler specified by "echo",
> then the value of $a (56) gets emitted to the client side.
>
> 4. Request terminates (and the control flow never goes outside of
>    the
> "if" block), just as in Case 1.
>
> Okay, you see how ngx_proxy module's config inheritance among nested
> locations take the key role here, and make you *believe* it works
> the way that you want. But other modules (like "echo" mentioned in
> one of my earlier emails) may not inherit content handlers in nested
> locations (in fact, most content handler modules, including upstream
> ones, don't).
>
> And one must be careful about bad side effects of config inheritance
> of "if" blocks in other cases, consider the following example:
>
> [Case 5]
>
> location /proxy {
> set $a 32;
> if ($a = 32) {
> return 404;
> }
> set $a 76;
> proxy_pass http://127.0.0.1:$server_port/$a;
> more_set_headers "X-Foo: $a";
> }
> location ~ /(\d+) {
> echo $1;
> }
>
> Here, ngx_header_more's "more_set_headers" will also be inherited by
> the implicit location created by the "if" block. So you will get:
>
> curl localhost/proxy
> HTTP/1.1 404 Not Found
> Server: nginx/0.8.54 (without pool)
> Date: Mon, 14 Feb 2011 05:24:00 GMT
> Content-Type: text/html
> Content-Length: 184
> Connection: keep-alive
> X-Foo: 32
>
> which may or may not what you want :)
>
> BTW, the "add_header" directive will not emit a "X-Foo" header in
> this case, and it does not mean no directive inheritance happens
> here, but add_header's header filter will skip 404 responses.
>
> You see, how tricky it is behind the scene! No wonder people keep
> saying "nginx's if is evil".
>
> Cheers,
> -agentzh
>
> Disclaimer: There may be other corner cases that I've missed here,
> and other more knowledgeable people can correct me wherever I'm
> wrong :)

Thank you for elaborating on this issue further. Lets see if I can
summarize my understanding, right now.

1. Inheritance in if blocks (which are in fact implicit locations) can
   happen *only* for a few modules, like proxy, fastcgi and your
   headers_more. Inheritance is the exception and not the rule. 
   Most modules don't provide content phase handlers inheritance.

2. As soon as there's a matching location, the rewrite phase starts
   and runs. As long as all conditions on the ifs are true the rewrite
   phase directives are always processed, no matter what they are,
   rewrites or variable assignments or break or returns.

3. This differs only on the content phase. There it can happen that
   inner if blocks can inherit content handlers from outer blocks.

4. If there's content handler inside the if block that's the one
   that's used. It may happen that the module that provides this
   handler can inherit content handlers from the outer blocks.

Analyzing your examples:

Case 1: There's no content handler inside the if block, but the proxy
        module provides inheritance hence we get the value 76.

Case 2: There's a content handler inside the if block. The echo module
        doesn't provide inheritance of directives in outer
        blocks. Hence the value is 76 not because of the proxy_pass
        directive but because of the echo "a = $a" directive.

Case 3: The if block has a break that interrupts the control flow. So
        the set $a 76 directive is never processed. We get 56 because
        of the content handler provided by the echo module inside the
        if block.

Case 4: (You named it 5, but it's a typo)

        The return directive inside the if block provides a special
        response: 404. The output filter that the headers_more module
        provides is inherited by the if and gets sent in the reply.

Now I see why in the http://wiki.nginx.org/IfIsEvil page the only
*safe* directives are return and rewrites with a last flag. Either one
of them "break" the control flow in the rewrite phase. Therefore are
not susceptible to inheritance issues.

Is this correct?

Thanks,
--- appa



More information about the nginx mailing list