Flask's SERVER_NAME, subdomains and 404 errors
Setting Flask's SERVER_NAME can give 404 errors if you are using subdomains.
This is a short post about Flask and the config variable SERVER_NAME. Like many developers I bumped into this at a certain moment, and I thought I share my story. Hopefully this will prevent headaches for some.
My websites must be available by typing the following addresses in the browser:
- https://example.com = 'without-www', and,
- https://www.example.com = 'with-www'
Whats more, for this website I had decided that the URLs in the generated pages must be 'without-www' or 'with-www', depending on what the request URL was. This means that if I type:
https://example.com/contact
there is no redirection and all links in the generated page will (appear to) start with https://example.com. Or, if I type:
https://www.example.com/contact
there is no redirection and all links in the generated page will (appear to) start with https://www.example.com.
I must add that this is not the best practice and that you always should use a single canonical name, meaning that your website is either 'with-www' or 'without-www', and never both! In a next post I will get into details about this.
Anyway, the above worked fine for all my websites except one, this website in fact. Months ago this was not a problem but it suddenly changed after a major code update. To prevent 404 errors for the 'with-www' version I solved this quickly by having Nginx redirect 'with-www' to 'without-www'. And then there were more urgent other projects and months passed.
Recently I was updating my (ISPConfig) server and thought I try this again. Of course still 404 errors for the 'with-www' version.
I made some time and decided to create the same situation on my development PC. This meant copying the Letsencrypt certificates to my PC and slighty modifying the development configuration by changing FLASK_ENV to 'production' and removing the 'with-www' to 'without-www' redirect in Nginx. I left FLASK_DEBUG=True so I could put breakpoints (exceptions) in the Python code and see what was going on.
Fortunately I experienced the same 404 errors when changing from 'without-www' to 'with-www' on my PC. Then, after some time I restarted Flask and now suddenly the 'with-www' version worked and the 'without-www' version gave 404 errors. What? The other way around?
Searching the internet for 'Flask 404' I bounced into pages, see links below, that hinted that the config variable SERVER_NAME could be the problem.
Then I remembered adding SERVER_NAME to my config when I was working on Celery tasks. I did this because I thought it could be useful to generate Jinja templates with url_for() links in some tasks. Later I discarded this idea. Tasks should be should straightforward and not contain bells and whistles. But I did not remove the SERVER_NAME code, because everything was running fine, and after setting redirection in Nginx there were more urgent other projects, what else is new.
Description of what happened
My factory.py (__init__.py) looked like:
def create_app():
...
@app.before_request
def before_request():
....
# get server_name from http_host
if current_app.config.get('SERVER_NAME') is None:
http_host = request.environ.get('HTTP_HOST')
current_app.config['SERVER_NAME'] = http_host
....
Here I get SERVER_NAME from the request if it has not been set. This means once config[SERVER_NAME] is set it is not updated any more. If I start with:
https://www.peterspython.com
then the SERVER_NAME is set to: www.peterspython.com. But if I start with:
https://peterspython.com
then the SERVER_NAME is set to: peterspython.com.
In both cases config remembers the SERVER_NAME between requests (of this session) so if I change a working 'with-www' URL to a 'without-www' URL, then we get the 404 errors because Flask routing uses a wrong SERVER_NAME.
The behavior of Flask's SERVER_NAME is documented well but fully understanding it another thing. My problem is also described in the article 'Things You Should Know About Flask SERVER_NAME', see links below:
'Once you set SERVER_NAME, Flask can only serve request from one single domain and return 404 for other domains. If SERVER_NAME = mydomain.com, it won’t serve request from www.mydomain.com ...'
Solution
The solution is of course to remove the line:
....
if current_app.config.get('SERVER_NAME') is None:
....
This will always set the SERVER_NAME config variable on every request. Or even better (?), remove these lines all together because I do not use SERVER_NAME anywhere.
Summary
At one point I added the config variable SERVER_NAME to my code and set this variable in before_request only when it was not already set. I forgot that my website could run with both 'with-www' and 'without-www' URLs and that the Flask config is persistent between requests of the same session. Once I got there it was easy to solve.
This would never have happened if I had chosen a single canonical name for my website. With this canonical name Flask would only have to deal with either 'with-www' or 'without-www' but never with both. In a next post I will explain why your canonical name should always be 'with-www'.
Anyway, lessons learned ... again.
Links / credits
SERVER_NAME configuration should not implicitly change routing behavior. #998
https://github.com/pallets/flask/issues/998
Things You Should Know About Flask SERVER_NAME
https://code.luasoftware.com/tutorials/flask/things-you-should-know-about-flask-server-name/
Unexplainable Flask 404 errors
https://stackoverflow.com/questions/24437248/unexplainable-flask-404-errors
Read more
Flask
Leave a comment
Comment anonymously or log in to comment.
Comments (2)
Leave a reply
Reply anonymously or log in to reply.
This has been clawing at me all afternoon. Thank you for sharing this.
A word of warning, changing the app config every before_request is sensitive to race conditions. So if you are using a threaded model of Flask execution, I would strongly advise against using the presented approach.
A small demo I tried to see if it would fit my needs in this gist: https://gist.github.com/eelkevdbos/14177eb9d72f5c96ed0f22ed64c30d19
Recent
- Hiding database UUID primary keys of your web application
- Don't Repeat Yourself (DRY) with Jinja2
- SQLAlchemy, PostgreSQL, maximum number of rows per user
- Show the values in SQLAlchemy dynamic filters
- Secure data transfer with Public Key encryption and pyNaCl
- rqlite: a high-availability and distributed SQLite alternative
Most viewed
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Connect to a service on a Docker host from a Docker container
- Using PyInstaller and Cython to create a Python executable
- SQLAlchemy: Using Cascade Deletes to delete related objects
- Flask RESTful API request parameter validation with Marshmallow schemas