Middleware
Tenant Middleware for django-omnitenant
This module provides the central middleware component responsible for tenant resolution and context management in a multi-tenant Django application.
The TenantMiddleware intercepts each request, determines the current tenant based on the request (using a configurable resolver), and establishes the tenant context for the entire request lifecycle.
Key Responsibilities
- Dynamically load and instantiate the configured tenant resolver
- Resolve the current tenant from each incoming request
- Establish and maintain tenant context throughout the request
- Handle domain resolution failures gracefully
- Set the tenant on the request object for downstream access
Architecture
- Uses Django's MiddlewareMixin for compatibility with Django's middleware system
- Loads resolver class dynamically based on settings.TENANT_RESOLVER
- Implements context management using TenantContext for thread-safe tenant isolation
- Provides automatic fallback to PUBLIC_TENANT for requests from public hosts
Configuration
The middleware is configured through Django settings:
MIDDLEWARE = [ # ... other middleware 'django_omnitenant.middleware.TenantMiddleware', ]
OMNITENANT_CONFIG = { 'TENANT_RESOLVER': 'myapp.resolvers.CustomResolver', 'PUBLIC_HOST': 'example.com', 'PUBLIC_TENANT_NAME': 'public', }
Usage
The middleware is automatically executed by Django for every request so that the current tenant can be accessed in the views:
def my_view(request):
current_tenant = request.tenant
# Use current_tenant for database queries, etc.
Error Handling
- Invalid domains return a 400 JSON response
- Public host requests fall back to the public tenant
- Resolver import errors are raised with descriptive messages
TenantMiddleware
Bases: MiddlewareMixin
Django middleware for resolving and managing tenant context.
This middleware is the core component that enables multi-tenancy in django-omnitenant. It executes on every request to: 1. Determine the current tenant based on the request 2. Set up the tenant context for the request lifecycle 3. Attach the tenant to the request object
The middleware uses a pluggable resolver pattern, allowing different tenant resolution strategies (subdomain-based, custom domain-based, header-based, etc.) to be configured without changing the middleware code.
Attributes:
| Name | Type | Description |
|---|---|---|
resolver |
Instance of the tenant resolver class configured in settings |
Lifecycle
- init: Loads and instantiates the resolver class
- call: Processes each request to resolve tenant and set context
Configuration
settings.TENANT_RESOLVER: Dotted path to resolver class (e.g., 'app.resolvers.SubdomainResolver') settings.PUBLIC_HOST: Domain name for public/shared content settings.PUBLIC_TENANT_NAME: Identifier of the public tenant
Examples:
In Django settings:
MIDDLEWARE = [
# ... other middleware
'django_omnitenant.middleware.TenantMiddleware',
]
OMNITENANT_CONFIG = {
'TENANT_RESOLVER': 'myapp.resolvers.SubdomainResolver',
'PUBLIC_HOST': 'example.com',
'PUBLIC_TENANT_NAME': 'public',
}
In views:
def my_view(request):
tenant = request.tenant # Automatically set by middleware
# Perform tenant-scoped operations
return Response({'tenant': str(tenant)})
Raises:
| Type | Description |
|---|---|
Exception
|
If the resolver class cannot be imported or instantiated |
Note
This middleware must be placed before any middleware that accesses tenant-specific data to ensure the tenant context is properly established.
Source code in django_omnitenant/middleware.py
73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | |
__init__(get_response=...)
Initialize the TenantMiddleware and load the configured resolver.
This constructor dynamically imports and instantiates the resolver class specified in settings.TENANT_RESOLVER. The resolver is responsible for determining which tenant a request belongs to.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
Callable[[HttpRequest], HttpResponse] | None
|
Callable that handles the request after middleware processing. Provided by Django's middleware loading mechanism. |
...
|
Raises:
| Type | Description |
|---|---|
Exception
|
If the resolver module or class cannot be found, or if instantiation fails. The error message includes details about the import failure. |
Process
- Parse the dotted path: 'module.path.ClassName' -> ('module.path', 'ClassName')
- Import the module
- Extract the class from the module
- Instantiate the resolver
- Call parent init with get_response
Example
If settings.TENANT_RESOLVER = 'myapp.resolvers.SubdomainResolver', this will import myapp.resolvers and instantiate SubdomainResolver()
Source code in django_omnitenant/middleware.py
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
__call__(request)
Process the incoming request to resolve and set the current tenant.
This method is called for every incoming request. It: 1. Uses the resolver to determine the current tenant 2. Sets up the tenant context 3. Attaches the tenant to the request 4. Processes the request through the rest of the middleware/view chain 5. Cleans up the tenant context after response is generated
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
|
HttpRequest
|
The incoming HTTP request |
required |
Returns:
| Name | Type | Description |
|---|---|---|
HttpResponse |
The response from the rest of the middleware/view chain, or a 400 JSON error response if the domain is invalid |
Process
- Try to resolve tenant using the configured resolver
- If resolution fails:
- Check if request is from public host (settings.PUBLIC_HOST)
- If yes: Create public tenant instance
- If no: Return 400 error response
- Set up tenant context using context manager
- Attach tenant to request object
- Process request through rest of chain
- Return response (context automatically cleaned up)
Examples:
Successful tenant resolution (subdomain.example.com): - Resolver finds tenant for subdomain - Tenant context established - request.tenant = tenant object
Public host request (example.com): - Resolver fails to find specific tenant - Detects PUBLIC_HOST match - Falls back to public tenant - request.tenant = public tenant object
Invalid domain (random-domain.com): - Resolver fails to find tenant - Host doesn't match PUBLIC_HOST - Returns {"detail": "Invalid Domain"} with 400 status
Raises:
| Type | Description |
|---|---|
DomainNotFound
|
Raised by resolver when domain is invalid |
TenantNotFound
|
Raised by resolver when tenant cannot be located |
Note
The TenantContext.use_tenant() context manager ensures that: - The tenant is set in thread-local storage - Database routers can direct queries to correct database - The tenant is automatically cleaned up after response
Source code in django_omnitenant/middleware.py
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | |