2

I have a website running with a Python/Django/uWSGI/Nginx setup. I also use Certbot to enable https on my site. My redirects from non-www to www (e.g. "example.com" to "www.example.com") result in a "Bad Request (400)" message even though I couldn't spot any deviations from the Nginx/Certbot documentation. Here is the relevant part of my sites-available Nginx code:

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

    location = /favicon.ico { access_log off; log_not_found off; }
    location /static/ {
        root /home/myname/example;
    }

    location / {
        include        uwsgi_params;
        uwsgi_pass     unix:/run/uwsgi/activities.sock;
    }

    listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; #managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; #managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    if ($scheme != "https") {
        return 301 https://$host$request_uri;
    } # managed by Certbot

}

I found a similar StackOverflow answer (Nginx: redirect non-www to www on https) but none of the solutions worked for me. I have SSL certificates for both example.com and www.example.com. I also tried creating a separate 443 ssl server block for example.com based on the comments in that answer but it didn't work either. My sites-available and sites-enabled code is the same.

What am I doing wrong?

3 Answers3

2

It seems that server_name when translated to the $host variable selects the first in the list of server_name. Let me know if that works. I can't quite test this currently.

Try swapping server_name to server_name www.example.com example.com; as well as changing return 301 https://$host$request_uri; to return 301 https://$server_name$request_uri;

server {
    server_name www.example.com example.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    # SSL CERT STUFF.
    server_name example.com;
    return 301 https://www.$server_name$request_uri;
}

server {
    listen 443 ssl;
    # SSL CERT STUFF.
    server_name www.example.com;

    # LOCATION STUFF
}
Mitchell Walls
  • 1,041
  • 1
  • 8
  • 13
  • That was a copy and paste error on my part. The actual server code has a closing bracket. Editing the question now... – ThanksForTheHelpAgain Jul 03 '18 at 16:51
  • I tested the changes above and it seems to work. As a matter of fact this will most likely not translate https:// example.com to https:// www.example.com. This should get you started possibly. Always remember to try in a clear cache browser. – Mitchell Walls Jul 03 '18 at 17:03
  • Your solution solved half of the problem! `http://example.com` now redirects properly. However, `https://example.com` still returns the Bad Request (400) error. Do you have any recommendations as to how I can get the https non-www to www redirect working? – ThanksForTheHelpAgain Jul 03 '18 at 17:42
  • Okay I've updated again. The only possibly not working step might be the second return 301 `https://www.$server_name`$request_uri; might have to change to www.example.com – Mitchell Walls Jul 03 '18 at 18:01
  • It works! I went with `https://www.$server_name$request_uri`. Thank you! – ThanksForTheHelpAgain Jul 03 '18 at 19:44
  • I too had a similar issue. looks like certbot automatically updates config file. Without replacing `$host` to `$server_name`, I ran `ufw allow 'Nginx Full'` from [digital ocean tutorial](https://www.digitalocean.com/community/tutorials/how-to-set-up-let-s-encrypt-with-nginx-server-blocks-on-ubuntu-16-04) and it started working – rainversion_3 Apr 28 '21 at 21:27
1

This is not an efficient configuration for Nginx request processing. It's messy, your if condition gets evaluated on every request and I don't see where your non www to www is even meant to happen.

I'd split http and https:

server {
    listen 80 default_server;
    return 301 https://www.example.com$request_uri;
}

Thats all non https traffic taken care of in a single redirect. Now for the https:

server {
    listen 443 default_server ssl;
    server_name www.example.com;
    root # should be outside location blocks ideally
    ......
}

The default server directive means this server will handle any requests which do not have a matching server configuration. If you don't want that then add example.com after www.example.com, not before it. Any requests ending up here will display the first entry in the client browser bar.

Based on your comments you might need to add a separate block for the other domain to avoid an SSL certificate mismatch. Try this:

server {
    listen 443 ssl;
    server_name example.com;
    ssl_certificate .....;
    ssl_certificate_key .....;
    return https://www.example.com$request_uri;
}
miknik
  • 5,748
  • 1
  • 10
  • 26
  • Thanks for the input! Your code is giving the same result as Mitchell Walls' solution: `http://example.com` now redirects properly but `https://example.com` still returns the Bad Request (400) error. Any ideas as to why this is happening? – ThanksForTheHelpAgain Jul 03 '18 at 18:00
  • Do you have a single SSL certificate with both example.com and www.example.com on, or 2 certificates? – miknik Jul 03 '18 at 18:04
  • I think I have a single SSL certificate with both domains on it. – ThanksForTheHelpAgain Jul 03 '18 at 19:14
  • I edited my answer with another server block for you to try adding, if you visit the site in chrome, click the padlock in address bar, click certificate, details then choose Subject Alternative Name if will list all domains the certificate covers – miknik Jul 03 '18 at 19:26
  • Thanks for the tips! I checked the SSL using Chrome and it's one certificate covering both. I wound up using Mitchell Walls' edited solution, which was very similar to yours. I created a separate block for the other domain like you said, but when I used default_server for the first 2 blocks the same error still occurred. When I left it out and went with Mitchell Walls' code it worked perfectly. – ThanksForTheHelpAgain Jul 03 '18 at 19:49
0

Although the OP has accept one of the answers as the solution, I just want to point out that it may not be the best practice.

The correct way is to use $host instead of $server_name (as per Mitchell Walls' example) or hardcoded www.exmple.com (as per miknik's example). Both results an extra 443 server directive that is not necessary and messy.

server {
    listen 80 default_server;
    server_name www.example.com example.com;
    root /var/www/html;    # define your root directory here
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    # SSL CERT STUFF.
    #server_name www.example.com;    you don't need to specify again here

    # LOCATION STUFF
}

There is a difference between $host and $server_name:

  • $host contains "in this order of precedence: host name from the request line, or host name from the 'Host' request header field, or the server name matching a request".
  • $server_name contains the server_name of the virtual host which processed the request, as it was defined in the nginx configuration. If a server contains multiple server_names, only the first one will be present in this variable.
hcheung
  • 3,377
  • 3
  • 11
  • 23
  • Awesome about the host thing. Just curious about your example how do you define the ssl cert for both example.com and www.example.com. The OP has two different certs for each. So wouldn't your example run into an error when going directly to example.com with https? – Mitchell Walls Jan 13 '20 at 17:30