Middleware & Hooks

Intercept and transform requests and responses with composable middleware and lifecycle hooks.

Overview

Middleware wraps the request pipeline. Each middleware receives a ctx (RequestContext) and a next_handler callable, and can inspect, modify, or short-circuit the request.

Pre-Routing vs Post-Routing (v0.0.4)

Middleware runs in two phases controlled by the phase attribute:

from lcore import Lcore, CORSMiddleware, SecurityHeadersMiddleware

app = Lcore()

app.use(CORSMiddleware(allow_origins='*'))      # phase='pre'  runs before routing
app.use(SecurityHeadersMiddleware(hsts=True))     # phase='post'  wraps handler

Execution order within each phase is controlled by order (lowest first).

Built-in Middleware

MiddlewareOrderPhasePurpose
ProxyFixMiddleware-10preTrust X-Forwarded-* from reverse proxies
BodyLimitMiddleware0preEnforce request body size limit
RequestIDMiddleware1postAdd unique request ID header
RequestLoggerMiddleware2postStructured JSON request logging
CORSMiddleware3preCross-origin resource sharing (preflight before routing)
SecurityHeadersMiddleware5postSecurity headers to every response
CSRFMiddleware10preCSRF token generation and validation
TimeoutMiddleware-5postEnforce per-request time limit
CompressionMiddleware90postGzip response compression

RequestIDMiddleware

Adds a unique X-Request-ID header to every response and sets ctx.request_id:

from lcore import RequestIDMiddleware
app.use(RequestIDMiddleware())

RequestLoggerMiddleware

Logs every request as structured JSON with method, path, status, and duration:

from lcore import RequestLoggerMiddleware
import logging

logger = logging.getLogger('myapp')
app.use(RequestLoggerMiddleware(logger=logger))

CORSMiddleware

Full CORS support with origin validation and preflight handling:

from lcore import CORSMiddleware

# Allow all origins
app.use(CORSMiddleware(allow_origins='*'))

# Restrict to specific origins
app.use(CORSMiddleware(
    allow_origins=['https://myapp.com', 'https://admin.myapp.com'],
    allow_methods=['GET', 'POST', 'PUT', 'DELETE'],
    allow_headers=['Content-Type', 'Authorization'],
    expose_headers=['X-Request-ID'],
    allow_credentials=True,
    max_age=86400
))
ParameterDefaultDescription
allow_origins'*'Allowed origins: '*', single string, or list
allow_methods['GET','POST','PUT','DELETE','PATCH','OPTIONS']Allowed HTTP methods
allow_headers['Content-Type','Authorization','X-Requested-With']Allowed request headers
expose_headers[]Headers visible to the browser
allow_credentialsFalseAllow cookies/auth headers. Cannot be used with allow_origins='*' specify explicit origins instead.
max_age86400Preflight cache duration (seconds)

SecurityHeadersMiddleware

Adds best-practice security headers to every response:

from lcore import SecurityHeadersMiddleware

app.use(SecurityHeadersMiddleware(hsts=True, hsts_max_age=31536000))

# Override specific headers
app.use(SecurityHeadersMiddleware(
    hsts=True,
    **{'Content-Security-Policy': "default-src 'self'"}
))

Default headers set: X-Content-Type-Options: nosniff, X-Frame-Options: DENY, X-XSS-Protection: 1; mode=block, Referrer-Policy: strict-origin-when-cross-origin, Content-Security-Policy: default-src 'self'.

CSRFMiddleware

Validates CSRF tokens on state-changing requests. Tokens are checked in the header first, then in form data:

from lcore import CSRFMiddleware

app.use(CSRFMiddleware(
    secret='my-csrf-secret',
    cookie_name='_csrf_token',
    header_name='X-CSRF-Token',
    form_field='_csrf_token',
    safe_methods=('GET', 'HEAD', 'OPTIONS'),
    secure=True   # Set cookie with Secure flag (HTTPS only)
))
ParameterDefaultDescription
secretNoneHMAC secret. Auto-generated if not set (not persistent across restarts).
cookie_name'_csrf_token'Cookie name for the CSRF token.
header_name'X-CSRF-Token'Header to check for the token (AJAX).
form_field'_csrf_token'Form field name to check for the token.
safe_methods('GET','HEAD','OPTIONS')HTTP methods that skip CSRF validation.
secureFalseSet Secure flag on cookie (requires HTTPS).

BodyLimitMiddleware

Rejects requests with bodies exceeding the limit (returns 413):

from lcore import BodyLimitMiddleware

# Limit request bodies to 5MB
app.use(BodyLimitMiddleware(max_size=5 * 1024 * 1024))

CompressionMiddleware

Gzip-compresses responses when the client accepts it:

from lcore import CompressionMiddleware

app.use(CompressionMiddleware(
    min_size=256,       # Don't compress small responses
    level=6,            # Compression level (1-9)
    content_types=None  # Compress all content types
))

ProxyFixMiddleware

Tells request.remote_addr and request.urlparts to trust X-Forwarded-* headers from known reverse proxies. v0.0.4 alternative: set app.config['proxy.trusted'] instead.

from lcore import ProxyFixMiddleware

app.use(ProxyFixMiddleware(trusted_proxies=['127.0.0.1', '10.0.0.1']))
app.use(ProxyFixMiddleware(num_proxies=1))  # trust first proxy in chain

TimeoutMiddleware

Runs the handler in a thread pool, returning 503 if it exceeds the deadline. v0.0.4 moves it to post-routing phase.

from lcore import TimeoutMiddleware

app.use(TimeoutMiddleware(timeout=30))       # 30 second limit
app.use(TimeoutMiddleware(timeout=10, max_workers=8))

Custom Middleware

Create custom middleware by subclassing Middleware. Set phase = 'pre' to run before routing (for auth, rate limiting, body inspection) or leave the default 'post' to wrap the handler:

from lcore import Middleware
import time

class TimingMiddleware(Middleware):
    name = 'timing'
    order = 2
    phase = 'post'        # v0.0.4: default, wraps the handler

    def __call__(self, ctx, next_handler):
        start = time.time()
        result = next_handler(ctx)
        duration = time.time() - start
        ctx.response.set_header('X-Response-Time',
            f'{duration*1000:.2f}ms')
        return result

app.use(TimingMiddleware())

Short-Circuiting

Return a response early without calling next_handler:

from lcore import Middleware, HTTPResponse

class AuthMiddleware(Middleware):
    name = 'auth'
    order = 5

    def __call__(self, ctx, next_handler):
        token = ctx.request.get_header('Authorization')
        if not token or not self.validate(token):
            ctx.response.status = 401
            return {'error': 'Unauthorized'}
        ctx.user = self.decode(token)
        return next_handler(ctx)

    def validate(self, token):
        return token.startswith('Bearer ')

    def decode(self, token):
        return {'sub': token.split(' ')[1]}

Route-Specific Middleware

Apply middleware only to specific route patterns, or skip middleware on specific routes:

# Only applies to /api/* routes
app.use(AuthMiddleware(), routes='/api/*')

# Multiple patterns
app.use(RateLimitMiddleware(), routes=['/api/*', '/webhook/*'])

# v0.0.4: skip middleware on a per-route basis
app.post('/api/login', callback=login, skip=['csrf'])

The skip parameter on routes now works for middleware (not just plugins). The route's skiplist is checked during post-routing middleware execution.

Security: be intentional about skip=['csrf']

CSRF exemption is appropriate for: webhooks from Stripe/GitHub (they can't send your CSRF token), login endpoints (the client doesn't have a token yet), and routes using Bearer token auth (not cookie-based, so CSRF is irrelevant). Never skip CSRF on state-changing endpoints that use cookie-based sessions those are exactly what CSRF protects. Audit your route table: if a route with skip=['csrf'] transfers money, changes passwords, or modifies user data, you have a problem.

Middleware Hooks

For a cleaner separation of concerns, use MiddlewareHook with explicit pre() and post() methods:

from lcore import MiddlewareHook, HTTPResponse

class AuditHook(MiddlewareHook):
    name = 'audit'
    order = 3

    def pre(self, ctx):
        # Runs before the handler
        # Return HTTPResponse to short-circuit
        if ctx.request.path.startswith('/admin'):
            if not ctx.request.get_header('X-Admin-Key'):
                return HTTPResponse('Forbidden', status=403)

    def post(self, ctx, result):
        # Runs after the handler
        # Can modify and return the result
        log_access(ctx.request.path, ctx.response.status_code)
        return result

app.use(AuditHook())
MethodSignatureDescription
pre(ctx)Returns None or HTTPResponseBefore handler. Return response to short-circuit.
post(ctx, result)Returns the (possibly modified) resultAfter handler. Can transform the response.

Lifecycle Hooks

Hooks are lightweight callbacks fired at specific points during request processing:

@app.hook('before_request')
def log_request():
    print(f'{request.method} {request.path}')

@app.hook('after_request')
def add_header():
    response.set_header('X-Powered-By', 'Lcore')

All Hook Points

Hook NameTrigger PointDirection
before_requestBefore route matchingForward
after_requestAfter response is builtReversed
on_request_startVery start of request handlingForward
on_auth_resolvedAfter authentication resolvesForward
on_handler_enterJust before handler executesForward
on_handler_exitJust after handler returnsReversed
on_response_buildAfter response object is builtReversed
on_response_sendJust before response is sentReversed
on_module_mountWhen a sub-app is mountedForward
on_module_requestRequest to a mounted moduleForward
app_resetWhen app.reset() is calledForward
configWhen config changesForward
Reversed Hooks

Hooks marked "Reversed" fire callbacks in reverse registration order (LIFO). This ensures cleanup hooks run in the opposite order of setup hooks.

Programmatic Hook API

# Register
app.add_hook('on_request_start', my_callback)

# Remove
app.remove_hook('on_request_start', my_callback)

# Trigger manually
results = app.trigger_hook('on_request_start')

Module-Specific Hooks

Scope hooks to specific mounted sub-applications:

# Fire only for requests to /api/* routes
app.add_module_hook('/api/', 'before_request', check_api_key)

# Decorator form
@app.module_hook('/admin/', 'after_request')
def admin_log():
    log_admin_action(request.path)

Middleware Pipeline

The MiddlewarePipeline manages middleware execution order and caching:

Inspect the Stack

for mw in app.show_middleware():
    print(f"[{mw['order']:3d}] {mw['name']:20s} {mw['type']}")

# [  0] body_limit           BodyLimitMiddleware
# [  1] request_id           RequestIDMiddleware
# [  3] cors                 CORSMiddleware
# [  5] security_headers     SecurityHeadersMiddleware
# [ 90] compression          CompressionMiddleware

Remove Middleware

# Remove by instance
cors = CORSMiddleware()
app.use(cors)
app.middleware.remove(cors)