HTTP Server

Options on the application object that shape how HTTP is served. Configure them right after AustialFactory.create(...), before the application is initialized.

app = AustialFactory.create(AppModule)
app.set_global_prefix("api", exclude=["health"])
app.use_compression()
app.set_trust_proxy()

Global prefix#

app.set_global_prefix("api") prefixes every route: GET /cats becomes GET /api/cats. List routes that must stay put in exclude. An entry is a path (any method) or a RouteInfo:

from austial import RequestMethod, RouteInfo
 
app.set_global_prefix(
    "api",
    exclude=["health", RouteInfo("webhooks/stripe", RequestMethod.POST)],
)

With URI versioning the order is prefix, version, controller: /api/v1/cats.

Host-based routing#

A controller can serve only requests whose Host header matches. A :name segment captures a value, which HostParam reads:

from austial import Controller, Get, HostParam
 
 
@Controller("dashboard", host=":tenant.example.com")
class TenantDashboard:
    @Get()
    async def home(self, tenant: str = HostParam("tenant")):
        return {"tenant": tenant}                      # acme.example.com -> {"tenant": "acme"}
 
 
@Controller("dashboard", host="admin.example.com")      # same path, a different host
class AdminDashboard:
    @Get()
    async def home(self): ...

host= also takes a list of patterns, and HostParam() with no name returns all captures as a dict. The port is ignored. A request whose host matches no controller gets a 404.

Client IP and reverse proxies#

Ip() returns the client’s address. Behind a load balancer that is the balancer’s address unless you trust its X-Forwarded-For header:

app.set_trust_proxy("10.0.0.0/8")        # or "*" to trust every upstream (only if nothing else can reach the app)
@Get("whoami")
async def whoami(self, ip: str = Ip()):
    return {"ip": ip}

Compression#

app.use_compression(minimum_size=500) gzip-compresses responses at least that large, for clients that send Accept-Encoding: gzip. If a reverse proxy already compresses, leave it off.

Static files#

app.use_static_assets(directory, prefix="/") serves a directory. It’s mounted after your API routes, so a controller route always wins over a file of the same path:

app.use_static_assets("public", prefix="/static")
app.use_static_assets("web/dist", prefix="/", html=True)    # serve index.html for directories (a SPA)

Extra keyword arguments go to Starlette’s StaticFiles.

HTTPS, the listening URL and the HTTP adapter#

listen forwards extra keyword arguments to uvicorn, so TLS is a matter of passing the files; afterwards get_url() tells you where the app is:

await app.listen(8443, ssl_certfile="cert.pem", ssl_keyfile="key.pem")
app.get_url()            # "https://127.0.0.1:8443"  (RuntimeError before listen() started)
app.get_http_server()    # the uvicorn.Server, e.g. to set should_exit

Behind a TLS-terminating proxy you usually want plain HTTP here and set_trust_proxy() above instead.

A provider that needs the underlying FastAPI app (to mount a sub-application, add a route) injects HttpAdapterHost:

from austial import HttpAdapterHost
 
@Injectable()
class Docs:
    def __init__(self, host: HttpAdapterHost):
        self.host = host
 
    def on_application_bootstrap(self):
        self.host.http_adapter.mount("/legacy", legacy_wsgi_app)     # http_adapter is the FastAPI instance

AWS Lambda#

create_lambda_handler wraps the application with Mangum for API Gateway (REST and HTTP APIs), Function URLs and ALB:

# handler.py        (Lambda handler setting: handler.handler)
from austial import create_lambda_handler
from src.app.app_module import AppModule
 
handler = create_lambda_handler(AppModule)       # extra keyword arguments go to mangum.Mangum

Install the extra into the deployment package (pip install 'austial[lambda]', which brings in mangum). The application is built once per cold start; lifecycle hooks and async providers run during the lifespan startup on the first invocation. Keep initialisation light (a long on_module_init is a slower cold start), and don’t rely on in-memory state between invocations. Microservices, WebSocket gateways and austial.schedule jobs need a long-running process and don’t belong in a Lambda.

Sessions and other ASGI middleware#

app.use(MiddlewareClass, **options) adds any ASGI middleware. For example, cookie sessions (pip install itsdangerous), read with the Session() parameter helper:

from starlette.middleware.sessions import SessionMiddleware
 
from austial import Session
 
app.use(SessionMiddleware, secret_key=config.get_or_throw("SESSION_SECRET"))
 
 
@Get("visit")
async def visit(self, session: dict = Session()):
    session["visits"] = session.get("visits", 0) + 1
    return session

Session("key") returns just that key.

@All#

@All("path") registers a handler for every HTTP verb on the path, useful for catch-all proxies and webhook endpoints that don’t care about the method.