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:
there is no redirection and all links in the generated page will (appear to) start with https://example.com. Or, if I type:
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:
then the SERVER_NAME is set to: www.peterspython.com. But if I start with:
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 ...'
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.
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
Things You Should Know About Flask SERVER_NAME
Unexplainable Flask 404 errors
- Connect to a service on a Docker host from a Docker container
- AIOHTTP: Detecting DNS timeout with custom nameservers
- Flask Message Flashing: Replace Bootstrap Alerts by Toasts
- SQLAlchemy: Using Cascade Deletes to delete related objects
- SQLAlchemy PostgreSQL: Add a second BigInteger Primary Key
- Redirect on an exception in Flask using a decorator
- Flask SQLAlchemy CRUD application with WTForms QuerySelectField and QuerySelectMultipleField
- Using Python's pyOpenSSL to verify SSL certificates downloaded from a host
- Using UUIDs instead of Integer Autoincrement Primary Keys with SQLAlchemy and MariaDb
- Flask RESTful API request parameter validation with Marshmallow schemas
- Documenting a Flask RESTful API with OpenAPI (Swagger) using APISpec
- SLQAlchemy dynamic query building and filtering including soft deletes