FastAPI / Advanced: lifecycle and reliable service design / Lesson 17
Settings, hosts, and deployment boundaries
Validate configuration and expose only deliberate application behavior while separating trusted hosting assumptions from in-process tests.
On this page
Lesson goals
- Explain the assumptions and boundaries of settings, hosts, and deployment boundaries.
- Interpret both complete examples and justify their observed outputs.
- Diagnose the listed failures and apply the worked solutions while keeping identity, state, and lifecycle ownership explicit.
Configuration is another untrusted input
Environment variables, configuration files, and launch arguments can be missing, malformed, or selected for the wrong environment. Validate required fields, formats, bounds, and precedence before announcing readiness. A Pydantic BaseModel does not automatically load the process environment; settings integrations have their own package and behavior. The first example receives an explicit synthetic settings object and constructs an isolated app. That makes configuration deterministic and testable without reading real credentials or project files. Do not silently use a production database URL or empty secret as a fallback for a missing required setting.
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, ConfigDict, Field, ValidationError
class Settings(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
max_items: int = Field(gt=0, le=100)
def create_app(settings: Settings):
app = FastAPI(debug=False)
@app.get("/public-config")
def public_config(): return {"max_items": settings.max_items}
return app
with TestClient(create_app(Settings(max_items=20))) as client:
print(client.get("/public-config").json())
try:
Settings(max_items=0)
except ValidationError:
print("configuration rejected")
{'max_items': 20}
configuration rejected
The app receives only a synthetic validated value and exposes one deliberately public field. No environment, settings file, secret, or service is read.
Keep secrets separate from public settings
A secret-like field needs an approved runtime source, access policy, rotation, and redaction. A masked representation such as a secret wrapper helps selected display paths but does not turn an entire model dump, log, or response into safe storage. Return only deliberately public configuration fields and avoid exposing full settings through a debug endpoint. Test error messages with synthetic values to ensure they identify a field without echoing a credential. Configuration revision and secret-version reference can be useful diagnostics, while the underlying value remains outside logs, source, screenshots, and teaching fixtures.
Define host and proxy trust explicitly
TrustedHostMiddleware can reject unsupported Host values under its documented policy. The second example verifies an allowed in-process host and a rejected reserved hostname without DNS or network access. Host validation does not authenticate a user or define which proxy headers the server may trust. TLS termination, forwarded headers, root_path, path rewriting, and client address handling need a deployment-specific contract. Do not let an arbitrary incoming header define a security-sensitive absolute URL or principal. Verify the real proxy and server configuration separately in an authorized environment; TestClient does not reproduce those infrastructure boundaries.
from fastapi import FastAPI
from fastapi.testclient import TestClient
from starlette.middleware.trustedhost import TrustedHostMiddleware
app = FastAPI()
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["testserver"])
@app.get("/health")
def health(): return {"ok": True}
with TestClient(app) as client:
print(client.get("/health").status_code)
print(client.get("http://other.example.invalid/health").status_code)
200
400
The test host succeeds and the unsupported Host is rejected with 400. Both URLs use TestClientโs local transport; no hostname is resolved or contacted.
Use independent resource and transport limits
Field constraints and a maximum item count do not establish a maximum request-body size, upload size, response size, execution time, or concurrent work. Apply appropriate limits at the selected server, proxy, ASGI, and application boundaries and test their documented behavior. Streaming uploads and responses require bounded ownership rather than buffering everything in memory. A worker count changes process and dependency demand, while pools and in-memory caches remain process-local unless designed otherwise. Avoid universal worker, timeout, or memory prescriptions; choose values from representative authorized measurements and an explicit workload and failure budget.
Separate readiness, rollout, and recovery
A healthy app response is only one observation. Production readiness needs valid configuration, appropriate dependency state, safe admission, and a defined shutdown contract. A release should identify the verified artifact, dependency baseline, schema compatibility, settings revision, and credential reference. Startup and graceful termination need bounded ownership and a platform grace period, while rollback must remain compatible with current data. No server process, proxy, deployment, cloud action, real environment variable, credential store, or configured database is used here. The examples prove local validation and host policy only and do not claim a secure or production-ready deployment.
Common errors and fixes
A full settings dump is exposed for debugging
- Why it happens
- Public configuration and secret-bearing state were not separated.
- How to fix it
- Expose only allowlisted safe fields and remove sensitive output.
- Prevent it next time
- Test diagnostics and validation redaction with synthetic values.
A query bound is called an upload-size defense
- Why it happens
- Field validation and transport budgets were conflated.
- How to fix it
- Apply and verify the appropriate byte and lifecycle limits.
- Prevent it next time
- Document the boundary and unit of each limit.
Proxy headers are trusted from every caller
- Why it happens
- Deployment trust was inferred from header presence.
- How to fix it
- Define the authorized proxy and server trust contract.
- Prevent it next time
- Test the real hosting boundary separately from local ASGI.
Check your understanding
1. Does a successful TrustedHost test prove a user is authorized?
Show worked solution
No. It verifies a Host-policy boundary only. Authentication, resource authorization, TLS, proxy trust, and data access are distinct responsibilities. Test each under its actual scope instead of treating one middleware as a complete security model.
2. Why should startup reject a missing required credential instead of defaulting to an empty string?
Show worked solution
A silent fallback can announce readiness and fail later or contact the wrong system. Validate the configuration contract early, report the field name safely, and keep readiness false until required dependencies and settings are established.
Key takeaways
- Environment variables, configuration files, and launch arguments can be missing, malformed, or selected for the wrong environment.
- TrustedHostMiddleware can reject unsupported Host values under its documented policy.
- A healthy app response is only one observation.