encode/starlette
github.com/encode/starlette ↗ · 143 files · 1.0 MB
[source preview omitted from public export]
Overview
Starlette is a Python ASGI framework and toolkit for developers building asynchronous HTTP and WebSocket services. At commit 4e7fc04e5f2c69f60e418ff2107d0f6d9a0fe423, its composable application, routing, middleware, request, and response objects support both complete applications and independently mounted components. AnyIO provides concurrency boundaries, while optional packages supply forms, templates, signed sessions, telemetry, and testing; Cloudflare configuration serves the documentation site.
System map
The runtime contract is ASGI: a server supplies a mutable scope and asynchronous receive/send channels. Starlette composes a router and middleware, while individual components can also act as ASGI applications. State is held in application or connection mappings, upload temporary files, and signed client cookies; database selection belongs to consuming applications. Packaging and documentation deployment are separate from serving developer applications.
- ASGIApp is Callable[[Scope, Receive, Send], Awaitable[None]]; users instantiate Starlette and supply an external ASGI server, illustrated by uvicorn main:app.
- Starlette.__call__ sets scope['app'] and lazily caches a stack ordered as ServerErrorMiddleware, optional RequestBodyLimitMiddleware, user middleware, ExceptionMiddleware, then Router.
- Router.app handles HTTP, WebSocket, and lifespan scopes; Route, WebSocketRoute, Mount, and Host provide endpoint adaptation, subapplication composition, and host-based dispatch.
- Synchronous endpoints and background callbacks cross into AnyIO worker threads through run_in_threadpool; synchronous streaming iterators use iterate_in_threadpool.
- State wraps an in-memory dictionary, multipart uploads use SpooledTemporaryFile, and SessionMiddleware stores signed JSON in cookies; documentation leaves database implementation to the consuming application.
- The package requires Python >=3.11, anyio>=4,<5, and conditional typing_extensions; the full extra adds itsdangerous, Jinja2, OpenTelemetry API, python-multipart, PyYAML, httpx, and httpx2.
- Hatchling builds the distribution using the version in starlette/__init__.py; scripts/install uses uv sync --frozen, scripts/check runs version consistency, Ruff and mypy checks, and scripts/build runs uv build, Twine, and Zensical.
- Publish CI builds package and documentation artifacts, publishes PyPI only for tags, and deploys documentation to GitHub Pages and Cloudflare; wrangler.toml points Cloudflare assets at ./site.
Feature catalog
The public surface centers on developer APIs for composing web services. Routing supports function endpoints, endpoint classes, mounted applications, typed parameters, method restrictions, and reverse URLs. Requests, responses, WebSockets, rendering, authentication, and middleware provide the main service capabilities. Lifecycle handlers, background callbacks, configuration, and an in-process client complete the inspected toolkit.
- Route supports typed path matching, names, method sets, automatic HEAD inclusion for GET, per-route middleware, and body limits; HTTPEndpoint dispatches method handlers including QUERY and falls back from HEAD to get.
- Request exposes URLs, headers, repeated query parameters, cookies, client addresses, streamed or buffered bodies, JSON, and forms; Config resolves prefixed environment values before file values and defaults, with optional casts.
- Response variants supply HTML, text, compact JSON, redirects, streaming, and files; cookie APIs support expiry, Secure, HttpOnly, SameSite, and partitioning, with Response.set_cookie partitioning restricted to Python >=3.14.
- WebSocket supports acceptance with subprotocols and headers, text/bytes/JSON send and receive, asynchronous iterators, close reasons, and HTTP denial responses when the server advertises websocket.http.response.
- StaticFiles serves configured directories or package assets with HTML index/404 handling and conditional responses; Jinja2Templates adds rendering and context processors, while SchemaGenerator builds OpenAPI paths from YAML docstrings and gates QUERY on OpenAPI >=3.2.
- AuthenticationMiddleware delegates identity to an AuthenticationBackend and exposes auth/user through scope; requires enforces scopes with HTTP errors or named-route redirects, while SessionMiddleware provides signed cookie sessions.
- Middleware includes CORS preflight/private-network policy, GZip negotiation and media exclusions, trusted-host validation, HTTPS/WSS redirection, total request-body limits, and experimental OpenTelemetry HTTP spans.
- Lifespan context managers manage startup/shutdown resources; BackgroundTask and BackgroundTasks attach post-response callbacks, and TestClient offers synchronous HTTP calls, WebSocket sessions, asyncio/trio backends, and lifecycle management.
Data flow
ASGI scope and message streams carry input through middleware and routing into connection objects. Parsing and rendering are generally lazy or explicit, with cached request bodies distinct from consumed streams. Uploads, lifespan state, and sessions have different ownership and cleanup boundaries. Outputs pass through send wrappers, which can modify headers, enforce limits, or collect telemetry before reaching the server.
- Router.app records scope['router'] and the selected route, then merges child_scope; Route.matches converts regex captures into typed path_params, while Mount.matches updates root_path and preserves app_root_path.
- Request.stream consumes http.request chunks until more_body is false and raises ClientDisconnect on disconnect; body joins and caches chunks, and json decodes that cached body with json.loads.
- Request._get_form dispatches by Content-Type; FormParser percent-decodes URL-encoded field names and values into FormData, enforcing field count and combined encoded name/value size through parser callbacks.
- Multipart filename parts become UploadFile objects backed by spooled temporary files; callbacks queue bytes, parse awaits writes and rewinds completed files, and UploadFile offloads writes that would roll onto disk.
- Multipart parsing closes all tracked temporary files on BaseException; successful form ownership passes to FormData, whose close method closes uploads, and Request.close delegates to it.
- Router.lifespan merges yielded mappings into lifespan scope state; connection.state wraps the request scope dictionary, and TestClient copies app_state shallowly into HTTP and WebSocket scopes.
- SessionMiddleware verifies timestamped cookie signatures and decodes base64 JSON into scope['session']; response-start handling adds Vary on access, signs modified nonempty sessions, and expires cleared existing sessions.
- Response renders bytes before sending start/body messages, while StreamingResponse emits chunks with more_body; TestClient captures these messages into BytesIO and returns a buffered httpx-compatible Response, discarding HEAD bodies.
Behavior traces
The inspected flows follow explicit protocol transitions rather than a standalone executable entrypoint. Startup establishes resources, HTTP routing adapts endpoints, and session persistence occurs when response headers start. Upload and static-file paths cross filesystem boundaries with distinct cleanup and containment rules. WebSocket and exception paths enforce state-dependent behavior that is material to callers.
- Startup: TestClient.__enter__ starts a BlockingPortal and lifespan task → wait_startup sends lifespan.startup → Starlette.__call__ builds its stack → Router.app selects lifespan → Router.lifespan enters the context and sends startup.complete; exit triggers shutdown and context teardown, with failures distinguished by startup state.
- HTTP success: Starlette.__call__ → middleware stack → Router.app → Route.matches → scope update → Route.handle → request_response creates Request → endpoint runs asynchronously or in a worker thread → returned Response sends headers/body and then awaits its background callback.
- Routing fallback: Router.app returns on the first FULL match, remembers the first PARTIAL match for 405, otherwise tries a slash-adjusted scope and RedirectResponse, then calls default; unmatched WebSockets close and unmatched HTTP inside Starlette raises HTTPException(404).
- Upload: endpoint calls Request.form → _get_form selects MultiPartParser → parse streams chunks into callbacks → on_headers_finished creates UploadFile → queued writes and seek(0) complete → FormData returns; parser failures close temporary files, while form context exit closes successful uploads.
- Session persistence: SessionMiddleware loads and verifies an incoming cookie → request.session marks access → Session.__setitem__ marks modification → endpoint returns Response → send_wrapper observes http.response.start and signs JSON into Set-Cookie; read-only access adds Vary without rewriting the cookie.
- Static delivery: Mount updates root_path → StaticFiles.__call__ checks configuration and normalizes the path → get_response runs lookup_path in a thread → file_response selects 304 or FileResponse → FileResponse handles HEAD, pathsend, full content, or byte ranges; malformed and unsatisfiable ranges return 400 and 416.
- WebSocket endpoint: WebSocketRoute adapts a function through websocket_session → endpoint calls WebSocket.accept, which first receives connect if necessary → receive_text/bytes/json validates accepted state → send methods validate application state → close transitions to disconnected; denial responses require the advertised server extension.
- Error handling: ExceptionMiddleware installs handlers in scope → wrap_app_handling_exceptions chooses HTTP status then exception-MRO handlers, rejecting replacement after response start → unhandled HTTP errors reach ServerErrorMiddleware, which emits a debug/custom/default 500 only before start and always re-raises.
Testing signals
Tests and CI were inspected, not executed; no passing result or measured coverage is established here. The inspected suite uses pytest, AnyIO backends, in-process TestClient calls, and direct ASGI message tests. Assertions cover lifecycle, body-limit branches, upload cleanup, file containment, ranges, sessions, and background execution. Coverage excludes selected branches, and this bounded inspection does not establish exhaustive scenario or deployment coverage.
- The pytest harness builds TestClient with AnyIO backend fixtures and optionally activates BlockBuster for Starlette blocking-call detection; lifespan tests assert startup inside the client context and teardown after exit.
- Test Suite CI defines Ubuntu jobs for Python 3.11–3.15, installs dependencies, builds package/docs, runs scripts/test and scripts/coverage, and skips scripts/check specifically on Python 3.14; these definitions were inspected, not run.
- Coverage measures branches in both starlette and tests and scripts/coverage requires 100%; overloads, NotImplementedError, and explicit pragma exclusions limit what that gate measures, so README's coverage statement is not a verified result.
- Upload tests assert spool rollover occurs outside the application thread and temporary files close after stream failure or simulated disk-full OSError; these are executable assertions, not observed successes.
- Body-limit tests exercise rejection before receive, accumulated bytes without reliable Content-Length, failure after response start, enforcement before user middleware, and route limits that raise or lower the application limit.
- Static-file tests call ASGI helpers directly for traversal because the client normalizes paths; response tests assert 100-range acceptance, over-limit fallback to full 200, malformed-range 400, and unsatisfiable-range 416.
- Session tests distinguish modification-driven Set-Cookie from read-only access and verify Vary: Cookie; background tests assert sequential callbacks and that a failing first task prevents the next task from executing.
- CodSpeed CI selects ten benchmark modules in simulation,memory mode on Python 3.14 with fixed allocator/hash settings; routing fixtures compare 20/120-route tables using fresh scopes and status assertions. Neither benchmarks nor CI were executed, and in-process transport evidence does not establish live-server behavior.
Risk map
These findings are source observations and explicitly labeled hypotheses, not confirmed vulnerabilities. Material boundaries include optional body limits, client-readable signed sessions, filesystem serving, diagnostic output, and telemetry export. Middleware task boundaries and response-start state create compatibility-sensitive behavior. Existing tests cover several of these boundaries, but they were not executed during this inspection.
- Total request-body size is unlimited by default, Request.body buffers all chunks, and multipart max_part_size applies to non-file parts; spool_max_size is a disk-rollover threshold rather than a file-size cap. Nested limit responders replace the active limit rather than always choosing the minimum.
- Session payloads are base64 JSON protected by TimestampSigner, without encryption in this path; HttpOnly and SameSite=lax are defaults, while Secure requires https_only=True.
- Hypothesis: mutating a nested dictionary or list in request.session without assigning through Session can leave modified false and omit Set-Cookie, because tracking covers top-level dict operations rather than recursively wrapped values; the inspected tests do not settle this nested-mutation behavior. (inferred)
- StaticFiles rejects absolute paths and checks resolved containment by default, but follow_symlink=True uses lexical absolute paths; FileResponse separately accepts application-supplied filesystem paths without applying StaticFiles directory containment.
- debug=True sends exception tracebacks to clients, choosing HTML from the Accept header or plain text otherwise; ServerErrorMiddleware still re-raises after generating its response.
- Experimental OpenTelemetryMiddleware records query strings, client addresses, and user agents on spans and extracts incoming trace context; its API and emitted telemetry explicitly may change in minor releases without deprecation.
- BaseHTTPMiddleware introduces task-group and receive-cache boundaries; the suite explicitly marks backward context-variable propagation as xfail, and consumed streams are forwarded downstream as an empty body rather than replayed.
- BackgroundTasks stop on the first exception after response delivery, when exception handlers cannot replace started responses; the deprecated WSGI adapter also buffers the whole request and uses an unbounded memory stream, making these distinct compatibility and resource-review hotspots.
Extension points
Composition occurs through ordinary Python interfaces and ASGI callables. Route lists, converter registries, middleware factories, authentication backends, and lifespan context managers are explicit insertion points. Rendering and schema classes support specialization without replacing application dispatch. Optional integrations and mounted third-party applications keep concerns such as databases and GraphQL outside the core lifecycle.
- Custom routing components implement BaseRoute.matches, url_path_for, and handle; Router accepts an ordered route sequence and a custom default ASGI handler.
- register_url_convertor inserts Convertor instances into CONVERTOR_TYPES; compile_path consumes their regex and Route.matches uses convert, while reverse URL generation uses the converter contract.
- Middleware stores a factory and positional/keyword arguments; Starlette, Router, Route, and Mount construct wrappers in reversed declaration order, allowing scoped middleware composition.
- AuthenticationBackend.authenticate returns credentials/user or None, AuthenticationMiddleware accepts on_error, and BaseUser defines authenticated/display-name/identity properties for custom identity implementations.
- Exception mappings accept status codes or exception classes; the wrapper resolves class handlers through exception MRO, while Starlette reserves 500 and Exception mappings for outer server-error handling.
- HTTPEndpoint subclasses supply HTTP method functions; WebSocketEndpoint subclasses override on_connect, on_receive, on_disconnect, and encoding, with dispatch invoking those hooks around the receive loop.
- Response.render is a serializer override point; Jinja2Templates accepts a custom Environment and context processors, and BaseSchemaGenerator defines get_schema with reusable endpoint/docstring extraction.
- Lifespan supplies an async context manager that can yield shared resources; Mount accepts arbitrary ASGI applications, supporting external database ownership and third-party GraphQL integrations documented after built-in GraphQL removal.
Contributor handoff
This brief is based on bounded source inspection at the requested commit; tests, benchmarks, and CI were inspected, not executed. Start edits from the ASGI contract and the relevant implementation/test pair, then follow cross-module state and message ownership. Preserve differences between standalone components, composed applications, and server-supported extensions. Existing repository scripts define future validation, but the current task authorizes read-only inspection only.
- Use tests and workflow definitions as intended checks, not passing evidence: this investigation executed neither tests nor CI. TestClient invokes the application in process and buffers output, so its assertions have a different boundary from deployed server validation.
- Read types.py, applications.py, routing.py, requests.py, responses.py, and exception handling before cross-cutting changes; preserve ASGI scope mutation, lazy stack construction, and the response-start barrier.
- Routing edits must retain first-full-match precedence, partial-match 405 handling, slash redirects, mount root_path semantics, and reverse URLs; synthetic routing benchmarks provide early/late/miss/wrong-method scenarios for later authorized measurement.
- For request/upload changes, review stream reuse, total limits versus spool thresholds, worker-thread rollover, and cleanup on stream or disk failure; prioritize tests/test_requests.py, tests/test_formparsers.py, and tests/middleware/test_body_limit.py in a later executable task.
- Middleware changes require both HTTP and non-HTTP scope review; BaseHTTPMiddleware has a documented-in-test context propagation limitation, while session persistence and file delivery require their own mutation/header and containment/range checks.
- Follow uv.lock and scripts/install's frozen synchronization; preserve strict mypy and Ruff's 120-character line policy, and test optional dependency/import behavior when changing integrations, including TestClient's httpx2 preference and deprecated httpx fallback.
- For later authorized verification, the direct targeted form is uv run coverage run -m pytest <test-file>; scripts/test additionally invokes scripts/check and the global 100% coverage report outside GitHub Actions. A targeted run alone may not satisfy that global threshold.
- Documentation headings require explanatory text before snippets; version authority is starlette/__init__.py and sync-version compares it with release notes. Contributing docs contain stale examples naming tests/test_application.py and __version__.py, so verify actual tracked paths before following examples or release instructions.
Languages
| Language | Files | Share of source |
|---|---|---|
| Python | 84 | 98% |
| Markdown | 32 | docs/config |
| YAML | 8 | docs/config |
| HTML | 3 | docs/config |
| TOML | 2 | 2% |
| CSS | 1 | docs/config |
Start reading here
Probable source entrypoint: pyproject.toml — inferred from the directory tree and manifest, not the upstream docs.
Source roots
starlette/ (37 files)
Source layout — starlette/
| Path | Files |
|---|---|
starlette/middleware/ | 13 |
starlette/__init__.py | 1 |
starlette/_exception_handler.py | 1 |
starlette/_utils.py | 1 |
starlette/applications.py | 1 |
starlette/authentication.py | 1 |
starlette/background.py | 1 |
starlette/concurrency.py | 1 |
starlette/config.py | 1 |
starlette/convertors.py | 1 |
starlette/datastructures.py | 1 |
starlette/endpoints.py | 1 |
starlette/exceptions.py | 1 |
starlette/formparsers.py | 1 |
Repository structure
| Top-level path | Files |
|---|---|
starlette/ | 37 |
tests/ | 36 |
docs/ | 31 |
benchmarks/ | 14 |
.github/ | 9 |
scripts/ | 7 |
Docs found: AGENTS.mdLICENSE.mdREADME.mddocs/applications.mddocs/authentication.mddocs/background.mddocs/config.mddocs/contributing.mddocs/database.mddocs/endpoints.md
Package
pyproject.toml — starlette, 9 declared dependencies.
fullitsdangerous"jinja2"opentelemetry-api"python-multipart>=0.0.18"pyyaml"httpx>=0.27.0,<0.29.0"httpx2>=2.0.0"]
CI & config entrypoints
README.md— Repository readmeAGENTS.md— Agent instructions.github/workflows/publish.yml— GitHub Actions workflow.github/workflows/benchmark.yml— GitHub Actions workflow.github/workflows/zizmor.yml— GitHub Actions workflow.github/workflows/main.yml— GitHub Actions workflow
Structural health signals
Average score 7.6/10 across 49 analyzed source files; 6 hotspots surfaced.
starlette/testclient.py— 562 lines, score 5.9 (Large file, Deep nesting)starlette/routing.py— 765 lines, score 5.9 (Large file, Deep nesting)starlette/datastructures.py— 704 lines, score 6.2 (Large file, Deep nesting)
These are deterministic review leads — size, churn, and structural signals — not proof of a bug in this project.
Recent history
4e7fc04e5f2c2026-10-01 — Bump urllib3 from 2.7.0 to 2.8.0 (#3609)f0b8f03fa67f2026-10-01 — Bump the github-actions group with 4 updates (#3607)63c5760d8a672026-09-29 — docs: remove unnecessary f-strings from endpoint examples (#3596)2284ff008cef2026-09-27 — Stop file transfers when HTTP clients disconnect (#3523)85c3b7434fd62026-09-27 — Ignore Range when If-Range contains a weak ETag (#3567)