Application initialization

Table of contents

  1. Introduction
  2. How the app is built
  3. Running code at startup
  4. Choosing the configuration profile
  5. Customizing error pages
    1. The 403 page
    2. A request that is too large
  6. Changing how the app logs
  7. Exempting a view from CSRF
  8. Health probes
    1. Adding a readiness check
  9. Checking the task mode at start
  10. Failing on an incomplete feature
  11. Testing the app
  12. Advanced
  13. Reference
    1. The startup steps
    2. create_splent_app()
    3. Health probe settings
  14. 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

The 25 steps of create_splent_app, before, during and after the features 25 STEPS, ALWAYS IN THIS ORDER Before the features what a feature uses as it loads 1 Namespaces 2 Configuration 3 Reverse proxies 4 Security headers 5 Cookie security 6 Static cache 7 Database 8 Logging 9 Error pages 10 API errors 11 Locale 12 Jinja context 13 CSRF preparation 14 Features FeatureManager every declared feature, in the order of the UVL inject_config(app) init_feature(app) blueprints inject_context_vars translations CLI commands After the features what reads what they set 15 Page script 16 Storage 17 Sitemap 18 Sessions 19 Session cookies 20 Operator settings 21 Derived keys 22 Cookie policy 23 Background tasks 24 CSRF protection 25 Health probes
Steps 1 to 13 prepare what a feature uses while it loads. Steps 15 to 25 read what the features set, and the health probes wrap everything else.

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.

  1. Your product’s 403.html.
  2. An errors/403.html a feature ships, such as a theme.
  3. 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_LENGTH and 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


Back to top

splent. Distributed by an LGPL license v3. Contact us: drorganvidez@us.es