9

The Nginx docs warn in no uncertain terms that if is evil and should be avoided wherever possible, and there are similar warnings scattered across the length and breadth of the internet.

However, most of these warnings focus specifically on how badly if behaves in location blocks. Furthermore, the Nginx docs say that:

The only 100% safe things which may be done inside if in a location context are:

  • return ...;

  • rewrite ... last;

My question is: Is it safe to use an if at the level of the server block (rather than a location block) if the only directive contained therein is a return? For example, the following www-to-non-www redirect:

server {
    listen 80;
    server_name example.com www.example.com;

    if ($http_host != example.com) {
        return 301 http://example.com$request_uri;
    }

    # more config
}

Secondary question: I'm aware that the recommended method for doing this sort of thing is with two server blocks that have different values for server_name. If this is an acceptable use for if, is there any reason to still use two separate server blocks?

9
  • 1
    My understanding is two server blocks are best practice and perform better.
    – Tim
    Apr 5, 2017 at 3:09
  • @Tim - any idea why that would be? Is it simply that you're sidestepping the requirement of evaluating the if since that determination is made by the server-choosing logic, which would run in any case? Is that logic just more optimized than if? Apr 5, 2017 at 3:11
  • My assumption is it's just more efficient internally within Nginx. I'll probably leave it there because that's about the limit of my knowledge in this area, others will know more.
    – Tim
    Apr 5, 2017 at 3:13
  • "should be avoided wherever possible", from what I understand, they don't recommend "if" since there is a potential risk of doing a bad if statement, that leads to unexpected issues/problems/etc ?
    – Tolsadus
    Apr 5, 2017 at 8:11
  • Only my opinion, but I sort of suspect the whole point of nginx mentioning that 'if is evil' is precisely to avoid the kind of situation you are creating: there are ways of doing what you want that don't require evaluating an if clause for every request. That is the key difference: with two blocks, you are actually having the system do what you actually want, which is to make users use a specific (non-www) domain. With the if clause, you are bypassing nginx's mechanism for routing requests based on the Host: header, and implementing that routing yourself with if clauses Apr 9, 2017 at 9:21

4 Answers 4

2

One of the first steps when nginx receives a request is evaluating the HTTP Host header / TLS SNI field, and selecting the virtual host based on the result of that evaluation.

This means that the Host header is always processed.

Now, if you specify an if statement for the redirection, it means that the Host header will be checked twice, first to select the virtual host and then for checking the if condition. That is twice the work for the processor.

A counter-argument could be the memory consumption for a separate server block. However, the memory allocation is the same for the lifetime of the nginx process, while the double-evaluation of Host header happens on every request.

4

From "If is Evil":

In some cases, it’s also possible to move ifs to server level (where it’s safe as only other rewrite module directives are allowed within it).

This suggests that if in a server block is safe(r) than in a location block.

1

Not sure what you mean by safe, but let me tell about a case I ran into recently. I needed to deny access to a site unless a certain parameter is passed. Let's say the config looked like this:

server {
    server_name localhost;
    root /usr/share/nginx/html;
    try_files $uri /index.html;
}

I added an if:

    ...
    if ($arg_secret-key != 123456) {
        return 404;
    }

But nginx would give me 404 even if I provided the secret:

$ curl -sS localhost/?secret-key=123456

It turned out that try_files made an internal redirect to /index.html (not passing the parameter), which resulted in 404. So to make it work it needed to be this way:

try_files $uri /index.html$is_args$args;

But actually I had to deny access to the main page only:

    ...
    location = / {
        if ($arg_secret-key != 123456) {
            return 404;
        }
    }

And this way it worked w/o changing try_files. That's because there's also the index module. And try_files doesn't get inherited by the location block. As such, the index module makes the right redirect to /index.html?secret-key=123456, and for the new request there's nothing left to do for try_files.

If you're not sure what's happening, you might want to run nginx built with --with-debug and configure the error log with the debug level, e.g.:

nginx.conf:

error_log  /var/log/nginx/error.log debug;
server {
    server_name localhost;
    root /usr/share/nginx/html;
    ...
}
$ docker run --rm --name nginx -p 1111:80 -dv $PWD/nginx.conf:/etc/nginx/conf.d/default.conf nginx:alpine nginx-debug -g 'daemon off;'
// launch `docker logs -f nginx` in a separate console, use Enter to separate requests
// processing starts with "generic phase: 0"
$ curl -sS localhost:1111/?secret-key=123456
// change nginx.conf
$ docker exec nginx nginx -s reload
$ curl -sS localhost:1111/?secret-key=123456
...
$ docker stop nginx

More on it here.

About your second question, I think that in your case having 2 server's is preferable. Because that's suggested by developers, and if you choose the if version, one day you might run into some issue after changing the config. Or you might realize that in some cases it doesn't work the way you expected it to (in more complex cases). One might say, if introduces complexity into the config you might not know about. As such I'd use if if I had a good understanding of how it all works, or had no other option. And the if version looks like it might be worse performance-wise. Although that's just a speculation, and I can't provide any numbers. In my case I don't see any workaround, as such that's what I'm going to use for now.

-1

You can do it, but it's highly not recommended. There's already an official explanation about this.

To run multiple sites, I'd recommend making multiple configuration files for each site, then include the location in the main nginx.conf.

2
  • 1
    Joseph linked to the "if is evil" article in his question. I suspect he was hoping for a bit more detail, and more depth on using if vs using multiple server blocks. Your answer is slightly light on detail.
    – Tim
    Apr 5, 2017 at 4:09
  • 2
    Actually, the official docs suggest moving ifs to the server block when possible to avoid some of the known limitations.
    – kbolino
    May 6, 2019 at 18:05

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .