Application initialization
Table of contents
- Introduction
- How the app is built
- Running code at startup
- Choosing the configuration profile
- Customizing error pages
- Changing how the app logs
- Exempting a view from CSRF
- Health probes
- Checking the task mode at start
- Failing on an incomplete feature
- Testing the app
- Advanced
- Reference
- See also
Introduction
Every product builds its app with one call, create_splent_app(). It runs 25 steps in a fixed order, loads your features in the middle, and returns a Flask app ready to serve. You rarely change it, but it tells you where your code runs and what every app gets for free.
This is the src/<product>/__init__.py that product:create writes.
from splent_framework.app_factory import create_splent_app
def create_app(config_name=None):
return create_splent_app(__name__, config_name)
How the app is built
Your feature’s code runs in step 14, when FeatureManager calls its inject_config(app) and init_feature(app). Everything before it is ready by then, such as the config, db, Babel and the Jinja globals. Everything after it can read what your feature set. A step that raises stops the boot, and the log names the step. The startup steps lists them all.
Running code at startup
Put it in your feature’s init_feature(app). It runs once per boot, in load order, after the features you require.
from splent_framework.services.service_locator import register_service
def init_feature(app):
register_service(app, "NotesService", NotesService)
Code that every product of the line needs belongs in a feature, not in a product’s create_app(). Feature loading has the full order.
Choosing the configuration profile
Leave config_name empty and SPLENT_ENV picks the class of your product’s config.py. The CLI sets it for you.
SPLENT_ENV, else FLASK_ENV |
Class |
|---|---|
prod, production |
ProductionConfig |
test, testing |
TestingConfig |
dev, development, or anything else |
DevelopmentConfig |
Pass "development", "testing" or "production" to force one, as tests do with create_app("testing"). A product without config.py runs on the framework’s classes. A config.py that fails to import stops the boot, and names the package it needs. See Configuration.
Customizing error pages
Define a handle_<code>(app, e) in your product’s errors.py for any of 400, 401, 403, 404, 413 and 500.
from flask import render_template
def handle_404(app, e):
return render_template("errors/404.html"), 404
Without one, the framework renders <code>.html from your templates, or the plain error page when that is missing too. The 403 has rules of its own, below. A feature can register a handler of its own in init_feature, as a theme does for the 404, and it wins over the framework’s. Keep your 500 page self-contained, since the database may be what failed.
If drawing an error page fails, the app answers the plain page, or JSON to a client that asked for it, never an empty 500. Under /api/ every error is problem+json instead.
The 403 page
role_required answers 403 in every feature, so the framework always has a page for it. It takes the first of these.
- Your product’s
403.html. - An
errors/403.htmla feature ships, such as a theme. - The framework’s own page, inside your application shell (
base_template.html), so the account keeps its sidebar.
The framework’s page says “Access denied”, tells a signed-in account “Your account cannot open this screen.” and a visitor to sign in with an account that can, and offers “Back to the site”. It is translated, so a Spanish product shows “Acceso denegado”, and search engines are told not to index it.
A request that is too large
A body over MAX_CONTENT_LENGTH is refused before any view reads it. A client that sent JSON, or prefers it, gets this.
{"error": "The request body is larger than the 16 MB limit.", "code": "body_too_large"}
A browser gets your 413.html, or the plain 413.
Changing how the app logs
Define configure_logging(app) in your product’s logging.py. The one product:create writes logs to stderr and, when LOG_FILE is set, to a file that logrotate can rotate.
import logging
def configure_logging(app):
app.logger.setLevel(logging.INFO)
It may run more than once in one process, so never add a handler that is already there. Without the file, the app logs ERROR to a rotating app.log (or LOG_FILE) and, in debug mode, INFO to the console. That rotating file is not safe for several workers, so keep the scaffolded logging.py in production.
Exempting a view from CSRF
Every POST, PUT, PATCH and DELETE of every feature must carry a CSRF token. A view authenticated by a token, not by the session cookie, can leave the check.
from splent_framework.managers.csrf_manager import exempt
def init_feature(app):
exempt(api_bp, app)
exempt takes a blueprint or a view, from init_feature or as a decorator. Never exempt a view the session can reach. WTF_CSRF_ENABLED = False, as in TestingConfig, turns the check off. Forms and CSRF shows how a form or a fetch sends the token.
Health probes
Every app answers two probes before any feature sees the request, for GET and HEAD only.
GET /readyz
200 OK
{"status": "ok"}
| Probe | Asks | 200 when |
|---|---|---|
/healthz |
Is the process alive | Always, while it answers |
/readyz |
Can it serve | Every readiness check passes within SPLENT_READY_TIMEOUT seconds |
Otherwise /readyz answers 503 with {"status": "unavailable"}. Which check failed, and why, goes to the log and to the readiness listeners. The web service’s healthcheck and product:deploy ask /readyz (see product:build).
Adding a readiness check
The framework checks the database, the session store when it answers a ping, and Redis in the rq task mode. Add the service your feature needs in init_feature.
from splent_framework.health import register_readiness_check
def init_feature(app):
register_readiness_check("index", lambda: client.ping(), app)
A check passes when it returns anything but False and raises nothing. While a slow check is still running, the next probes answer 503 at once.
A probe runs no
before_request, opens no session and sets no cookie. A feature that redirects every request, such as a setup wizard, cannot make the process look dead.
Checking the task mode at start
The app reads TASKS_MODE once every layer of the config is in. An unknown mode, or rq without the package or a Redis, stops the app at start instead of at the first upload. In rq mode /readyz also checks that Redis. See Background tasks.
Failing on an incomplete feature
def create_app(config_name=None):
return create_splent_app(__name__, config_name, strict=True)
By default a feature without config.py, init_feature(app) or a blueprint still loads, with a warning for the blueprint. With strict=True each of those is a FeatureError. Use it in CI, or when a feature loads but serves nothing. Errors while loading lists every case.
Testing the app
app = create_app("testing")
TestingConfig sets TESTING, turns CSRF off, runs tasks in sync mode and uses MARIADB_TEST_DATABASE. Sessions still come from your session feature. The framework’s pytest fixtures do this for you, see Testing the framework.
Advanced
Flask runs after-request hooks in reverse order, and the security headers, the cookie flags and the asset cache register before the features. So a header or a cookie flag your feature sets is kept.
Features fill registries while they load, such as menus, assets, settings, file access and search. Each boot clears them first, so several apps built in one process see only their own features.
The deployment’s
MAX_CONTENT_LENGTHand cookie variables are applied again after the features, so a feature never overrules them.
Reference
The startup steps
| # | Step | What it does |
|---|---|---|
| 1 | Namespaces | Puts the src/ of each feature the product declares for SPLENT_ENV on sys.path, so a feature can import a declared sibling at module level. Nothing undeclared becomes importable |
| 2 | Configuration | Loads the class of your config.py, or the framework’s, into app.config |
| 3 | Reverse proxies | Trusts X-Forwarded-* from SPLENT_TRUSTED_PROXIES proxies. See Securing cookies |
| 4 | Security headers | Adds the security headers to every response, never replacing one already set |
| 5 | Cookie security | Marks every cookie Secure when the request came over HTTPS |
| 6 | Static cache | Versions asset URLs and caches them. See Caching assets |
| 7 | Database | Binds db, Flask-Migrate and the splent_migrations table. See Database |
| 8 | Logging | Your configure_logging(app), or the defaults. See Changing how the app logs |
| 9 | Error pages | The 400, 401, 403, 404, 413 and 500 handlers. See Customizing error pages |
| 10 | API errors | Errors under /api/ as problem+json. See HTTP APIs |
| 11 | Locale | Flask-Babel and the framework’s own catalog, before any feature’s. See i18n |
| 12 | Jinja context | The globals and filters of every template. See Template system |
| 13 | CSRF preparation | Creates the product’s CSRFProtect, so a feature can exempt a view while it loads |
| 14 | Features | Loads every feature in order. See Feature loading |
| 15 | Page script | Adds the framework’s page script to the layout.scripts slot of every shell |
| 16 | Storage | Serves /files/<feature>/... and adds flask storage. See Storage |
| 17 | Sitemap | Serves /sitemap.xml from what the features register. See Adding pages to the sitemap |
| 18 | Sessions | Flask-Session, with the SESSION_TYPE a feature set. Without one the app stops with a RuntimeError |
| 19 | Session cookies | The session cookie follows the request’s HTTPS like every other cookie |
| 20 | Operator settings | Applies the deployment’s MAX_CONTENT_LENGTH and cookie variables again, over any feature’s value |
| 21 | Derived keys | A key per purpose, with SPLENT_DERIVED_KEYS. See Signing with a key per purpose |
| 22 | Cookie policy | Warns when a SameSite=None cookie may go out without Secure, and names every view exempt from CSRF. See Sharing the session |
| 23 | Background tasks | Checks TASKS_MODE. See Checking the task mode |
| 24 | CSRF protection | Binds CSRFProtect with the features’ exemptions. See Exempting a view |
| 25 | Health probes | Wraps the whole app in /healthz and /readyz. See Health probes |
The order is _PIPELINE in app_factory.py.
create_splent_app()
| Argument | Default | Effect |
|---|---|---|
import_name |
Required | Your product package’s __name__ |
config_name |
None |
The profile. None follows SPLENT_ENV |
strict |
False |
An incomplete feature is a FeatureError. See strict mode |
extra_context |
None |
Variables added to every template |
Health probe settings
Each is read from the environment first, then from app.config.
| Setting | Default | Effect |
|---|---|---|
SPLENT_HEALTH_PATH |
/healthz |
Where the liveness probe answers. off turns it off |
SPLENT_READY_PATH |
/readyz |
Where the readiness probe answers. off turns it off |
SPLENT_READY_TIMEOUT |
3 |
Seconds a readiness probe may take |
See also
- Configuration, the settings every step reads.
- Feature loading, everything step 14 does.
- Testing the framework, the fixtures that build the app for tests.