Flask Application Creation Process
- 1. Introduction
- 2. Overview
- 3. Entry Point:
create_app() - 4. Detailed Execution Flow
- 5. Route Registration Process
- 6. Before Request Handlers
- 7. Before Request Execution Order
- 8. After Request Handlers
- 9. API Response Structure
- 10. Behavior based on SOGo state
1. Introduction
This document describes in detail the Flask application creation process in SOGo 6. This process is triggered in the /workspace/app/run.py file by the call:
app = create_app(sogo_state)
This function creates and configures the Flask application, registers all API routes, and sets up request/response interceptors (before_request and after_request handlers) that manage authentication, validation, and configuration loading.
2. Overview
The create_app() function performs several critical operations:
-
Flask Application Creation - Initializes the Flask application with configuration
-
API Registration - Registers two separate APIs (User API and Admin API)
-
Route Registration - Registers all endpoints from blueprints
-
Request Interceptors - Sets up before_request handlers for authentication, validation, and state checking
-
Response Interceptors - Sets up after_request handlers for response formatting
-
CORS Configuration - Enables Cross-Origin Resource Sharing for frontend development
3. Entry Point: create_app()
File: /workspace/app/init.py
def create_app(sogo_state: int) -> Flask:
"""
Create and configure the Flask application
"""
app = Flask(__name__)
app.config.from_object(process_config)
if not app.config.get("DO_SWAGGER"):
app.config.pop("BASIC_OPENAPI_URL_PREFIX")
app.config.pop("ADMIN_OPENAPI_URL_PREFIX")
flask_api = Api(app, config_prefix="BASIC_")
admin_api = Api(app, config_prefix="ADMIN_")
register_route(flask_api, cs.API_BASIC, sogo_state)
register_route(admin_api, cs.API_ADMIN, sogo_state)
CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}})
return app
4. Detailed Execution Flow
4.1. 1. Flask Application Instantiation
app = Flask(__name__)
app.config.from_object(process_config)
Creates a Flask application and loads configuration from process_config (ProcessSetting class), which includes:
-
Flask settings (SECRET_KEY, DEBUG mode, etc.)
-
Database configuration
-
Redis configuration
-
OpenAPI/Swagger settings
-
Authentication settings
4.2. 2. Swagger Configuration
if not app.config.get("DO_SWAGGER"):
app.config.pop("BASIC_OPENAPI_URL_PREFIX")
app.config.pop("ADMIN_OPENAPI_URL_PREFIX")
If Swagger documentation is disabled (DO_SWAGGER=False), removes OpenAPI URL prefixes from configuration to prevent documentation endpoints from being registered.
4.3. 3. API Instance Creation
flask_api = Api(app, config_prefix="BASIC_")
admin_api = Api(app, config_prefix="ADMIN_")
Creates two separate Flask-Smorest API instances:
-
User API (
flask_api): For regular users, uses configuration withBASIC_prefix -
Admin API (
admin_api): For administrators, uses configuration withADMIN_prefix
Each API has its own:
-
OpenAPI specification endpoint
-
Swagger UI endpoint (if enabled)
-
URL prefix
4.4. 4. Route Registration
register_route(flask_api, cs.API_BASIC, sogo_state)
register_route(admin_api, cs.API_ADMIN, sogo_state)
Registers all routes for both APIs. This process is detailed in the Route Registration Process section.
4.5. 5. CORS Configuration
CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}})
Enables Cross-Origin Resource Sharing for all /api/* endpoints, allowing requests from http://localhost:3000 (frontend dev).
5. Route Registration Process
The register_route() function orchestrates the complete registration of API routes and interceptors.
def register_route(flask_api: Api, name: str, sogo_state: int) -> None:
"""
Register all blueprints
"""
base_blueprint = Blueprint(name, name, url_prefix='/api')
register_after_request(base_blueprint)
register_before_request(base_blueprint, name, sogo_state)
for version, version_apis in all_apis.items():
version_blueprint = Blueprint(version, version, url_prefix=f'{name}/{version}')
basic_apis = version_apis[name]
for api in basic_apis:
version_blueprint.register_blueprint(api)
base_blueprint.register_blueprint(version_blueprint)
flask_api.register_blueprint(base_blueprint)
5.1. Hierarchy Structure
The registration creates a hierarchical structure:
Flask App
│
├── User API (flask_api)
│ └── base_blueprint (/api)
│ ├── before_request handlers
│ ├── after_request handlers
│ └── version_blueprint (/api/user/v1)
│ ├── auth_apis (authentication endpoints)
│ └── mail_apis (mail endpoints)
│
└── Admin API (admin_api)
└── base_blueprint (/api)
├── before_request handlers
├── after_request handlers
└── version_blueprint (/api/admin/v1)
└── admin_apis (admin endpoints)
6. Before Request Handlers
Before request handlers are executed before each request reaches the endpoint. They perform validation, authentication, and setup operations. The handlers registered depend on the API type and SOGo state.
6.1. Common Before Request Handlers
These handlers are registered for both User and Admin APIs, regardless of SOGo state.
6.1.1. 1. check_json_and_content_type()
Purpose: Validate JSON content for POST/PATCH/PUT requests
@base_blueprint.before_request
def check_json_and_content_type() -> ResponseReturnValue | None:
if request.method in {"POST", "PATCH", "PUT"}:
content_length = request.content_length
if content_length is not None and content_length == 0:
return None
if not request.is_json:
return create_api_base_response(error=err.ERROR_API_CONTENT_TYPE), 400
data = request.get_data(as_text=True)
try:
loads(data)
except (TypeError, JSONDecodeError):
return create_api_base_response(error=err.ERROR_API_NOT_JSON), 400
return None
Behavior:
-
Only applies to POST, PATCH, PUT methods
-
Allows empty body (content_length == 0)
-
Checks
Content-Type: application/jsonheader -
Validates that body is valid JSON
Error Responses:
| Error Code | Message | HTTP Status |
|---|---|---|
|
Request POST/PATCH/PUT Content-Type Is Not 'application/json' |
400 |
|
Request POST/PATCH/PUT Body Is Not A Json |
400 |
6.1.2. 2. get_user()
Purpose: Extract and validate user from authentication header
@base_blueprint.before_request
def get_user() -> ResponseReturnValue | None:
auth_header = request.authorization
user: User = UserAnonymous()
if auth_header:
if auth_header.type == 'bearer':
user = VoucherUserService(process_config).generate_user_from_voucher(auth_header.token)
elif auth_header.type == 'basic' and current_app.config[cs.ALLOW_AUTH_BASIC]:
pass
else:
return create_api_base_response(error=err.ERROR_WRONG_AUTHORIZATION_TYPE), 400
g.user = user
return None
Behavior:
-
Checks for
Authorizationheader -
Supports two authentication types:
-
Bearer token (JWT): Default production method
-
Basic auth: Only if
ALLOW_AUTH_BASICis enabled
-
-
If no authentication, creates
UserAnonymous()instance -
Stores user in Flask’s global
g.user
Authentication Flow:
Request with Authorization header
│
├── Bearer token?
│ ├── Yes → Validate JWT voucher
│ │ └── Create User instance
│ └── No
│ ├── Basic auth AND ALLOW_AUTH_BASIC?
│ │ └── Yes → (TODO: implement basic auth)
│ └── Other → ERROR_WRONG_AUTHORIZATION_TYPE
│
└── No Authorization header
└── Create UserAnonymous()
Error Responses:
| Error Code | Message | HTTP Status |
|---|---|---|
|
Header Authorization Incorrect Format |
400 |
User Object:
The g.user object contains:
-
uid: User unique identifier -
domain: User’s domain -
mail: User’s email address -
password: User’s password (for IMAP/SMTP connections) -
source_id: User source identifier -
authenticated: Boolean flag
6.2. User API Specific Handler
6.2.1. 3. check_non_anonymous_endpoint()
Purpose: Block anonymous users from protected endpoints
Only registered for: API_BASIC (User API)
@base_blueprint.before_request
def check_non_anonymous_endpoint() -> ResponseReturnValue | None:
anon_endpoints = {
"user.v1.ApiConfig.ApiAuthUserLogin",
"user.v1.ApiConfig.ApiAuthUserMode",
"user.v1.ApiConfig.ApiAuthUserCallback",
}
if isinstance(g.user, UserAnonymous) and request.endpoint not in anon_endpoints:
return create_api_base_response(error=err.ERROR_AUTHENTICATED_ROUTE), 401
return None
Behavior:
-
Defines whitelist of endpoints accessible to anonymous users
-
Checks if user is anonymous (
UserAnonymousinstance) -
Checks if endpoint is in whitelist
-
Blocks request if user is anonymous and endpoint requires authentication
Anonymous-Accessible Endpoints:
-
ApiAuthUserLogin: Login endpoint -
ApiAuthUserMode: Get authentication mode for domain -
ApiAuthUserCallback: OAuth callback endpoint
Error Responses:
| Error Code | Message | HTTP Status |
|---|---|---|
|
Anonymous User On Protected Endpoint |
401 |
6.3. State-Dependent Before Request Handlers
The following handlers are registered based on sogo_state.
6.3.1. When sogo_state == SOGO_NOT_INIT (State 1)
SOGo is functional but not yet configured. No system settings or default domain settings exist.
User API: block_sogo()
Purpose: Block all User API requests when SOGo is not initialized
@base_blueprint.before_request
def block_sogo() -> ResponseReturnValue:
return create_api_base_response(error=err.ERROR_SOGO_INIT), 412
Behavior:
-
Returns HTTP 412 (Precondition Failed) for all User API requests
-
User API is completely inaccessible until SOGo is configured
Users cannot use SOGo (check mail, send mail, etc.) until an administrator configures system settings.
Error Responses:
| Error Code | Message | HTTP Status |
|---|---|---|
|
Sogo Has Not Been Configured Yet |
412 |
Admin API: add_process()
Purpose: Add process configuration to Flask global context
@base_blueprint.before_request
def add_process() -> None:
if 'process' not in g:
g.process_settings = process_config
Behavior:
-
Adds
process_configtog.process_settings -
Allows admin endpoints to access process configuration
-
Admin can configure SOGo even when not initialized
Available in g:
-
g.user: Current user (admin) -
g.process_settings: Process configuration (database, Redis, etc.)
6.3.2. When sogo_state == SOGO_OK (State 2)
SOGo is fully operational with complete configuration.
Both APIs: get_config()
Purpose: Load all necessary configuration for each request
@base_blueprint.before_request
def get_config() -> None:
if 'process' not in g:
g.process_settings = process_config
system_settings, default_domain_settings = init_get_system_and_default_settings()
if 'system_settings' not in g:
g.system_settings = system_settings
if 'default_domain' not in g:
g.default_domain_settings = default_domain_settings
if 'user' in g:
#TODO retrieve domain settings relative to user domain here (for now just get default)
g.user_domain_settings = default_domain_settings
else:
logger.error("No user in Flask g")
raise AggravatedException("No user in Flask g")
Behavior:
-
Adds process configuration to
g.process_settings -
Loads system settings from database
-
Loads default domain settings from database
-
For authenticated users, loads domain-specific settings (currently uses default)
-
Raises critical error if user not found in
g(should never happen)
Available in g:
-
g.user: Current user -
g.process_settings: Process configuration -
g.system_settings: System-wide settings -
g.default_domain_settings: Default domain settings -
g.user_domain_settings: User’s domain settings (currently same as default)
Performance Note:
This handler queries the database on every request when SOGo is operational. This is by design to ensure settings changes are immediately reflected without restarting the application.
Future:
The TODO comment indicates that domain-specific settings retrieval will be implemented. Currently, all users get default domain settings.
7. Before Request Execution Order
The order of execution is critical and follows this sequence:
| Order | Handler | Applies To |
|---|---|---|
1 |
|
All APIs, all states |
2 |
|
All APIs, all states |
3 |
|
User API only, all states |
4 |
State-dependent handler |
Depends on |
7.1. Flow Diagram: Request Processing
HTTP Request
│
├─ Check Content-Type & JSON (POST/PATCH/PUT only)
│ ├─ Invalid → Return 400
│ └─ Valid → Continue
│
├─ Extract & Validate User
│ ├─ Invalid auth → Return 400
│ └─ Valid/Anonymous → Store in g.user
│
├─ [User API only] Check Anonymous Access
│ ├─ Anonymous + Protected endpoint → Return 401
│ └─ Authorized → Continue
│
├─ [State: SOGO_NOT_INIT]
│ ├─ User API → Return 412 (blocked)
│ └─ Admin API → Add g.process_settings → Continue
│
├─ [State: SOGO_OK]
│ └─ Load all configuration → g.process_settings, g.system_settings, g.default_domain_settings, g.user_domain_settings
│
└─ Execute endpoint handler
8. After Request Handlers
After request handlers are executed after the endpoint returns a response but before the response is sent to the client. They can modify the response.
8.1. bad_request_handler()
Purpose: Standardize error responses and format validation errors
Registered for: All APIs
@base_blueprint.after_request
def bad_request_handler(response: Response) -> ResponseReturnValue:
if response.status_code == 400:
if response.content_type == "application/json":
body = response.get_json()
# Check if the body is a SOGo one
try:
ApiBaseResponse().load(body)
except ValidationError:
response.set_data(dumps(
create_api_base_response(body, err.ERROR_VALIDATION_ERROR)
))
return response
Behavior:
-
Only processes responses with HTTP 400 status
-
Only processes JSON responses
-
Checks if response already follows SOGo’s API format (
ApiBaseResponse) -
If not (validation error from marshmallow), wraps it in standard format
Standard API Response Format:
All SOGo API responses follow this structure:
{
"data": <response_data_or_null>,
"error_code": "<error_code>",
"error_msg": "<error_message>"
}
Example Transformation:
Before (marshmallow validation error):
{
"email": ["Not a valid email address."],
"password": ["Field may not be blank."]
}
After (wrapped in SOGo format):
{
"data": {
"email": ["Not a valid email address."],
"password": ["Field may not be blank."]
},
"error_code": "S000300",
"error_msg": "Request Data Incorrect Format"
}
9. API Response Structure
All SOGo API responses use a consistent structure defined by ApiBaseResponse:
class ApiBaseResponse(Schema):
data : fields.Field = fields.Field(required=True, allow_none=True)
error_msg = fields.String(required=True)
error_code = fields.String(required=True)
10. Behavior based on SOGo state
The behavior of the application changes significantly based on SOGo state:
| State | Value | User API | Admin API |
|---|---|---|---|
|
0 |
Not reachable |
Not reachable |
|
1 |
Completely blocked |
Fully functional |
|
2 |
Fully operational |
Fully operational |