- resp.sse: JSON-encode dict/list data, comment frames ({"comment": ...}),
bytes passthrough, opt-in heartbeat= keepalive (queue+task merge so the
producer isn't cancelled mid-event), X-Accel-Buffering: no, and the
@resp.sse(heartbeat=...) decorator form. Refactored to _sse_frame /
_format_sse_event / _sse_with_heartbeat helpers.
- req.last_event_id exposes the Last-Event-ID header for stream resume.
- Docs + tests.
Backwards-compatible. 553 tests pass; ruff + mypy clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
51 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
v6.1.0 - 2026-06-29
A backward-compatible release that makes Server-Sent Events production-grade.
Added
- Production-grade
resp.sse. Server-Sent Events gain:- JSON-encoded data — a
datavalue that is adict/listis serialized automatically (great for structured/LLM streaming). - Comment frames via
{"comment": "..."}(and rawbytespass through verbatim). - Opt-in heartbeat —
@resp.sse(heartbeat=15)emits a keepalive comment during idle periods (without interrupting the producer mid-event), so long-lived streams survive proxy idle-timeouts. X-Accel-Buffering: noso events flush immediately behind nginx and similar proxies.- The
@resp.sse(heartbeat=...)decorator-with-arguments form.
- JSON-encoded data — a
req.last_event_idexposes the SSELast-Event-IDrequest header, so a handler can resume a stream where a reconnecting client left off.
v6.0.2 - 2026-06-29
A bugfix release closing a set of cross-feature interaction defects found by an
adversarial review of the 5.1->6.0 work — including two remotely-triggerable
500s and a bypass of max_request_size on multipart uploads. Upgrading from
6.0.x is strongly recommended.
Fixed
- Remote 500 on file endpoints. An
If-Modified-Sinceheader with a-0000timezone crashed every conditional file response (a naive-vs-aware datetime comparison sat outside the try/except). Both sides are now normalized to UTC. max_request_sizebypass + "Stream consumed" 500 on multipart. Parsing a form/file body (Form()/File()markers orreq.media("files")) now buffers through the size-checked body, so the cap is enforced (413) and the body stays readable afterward (req.content,media("form"), …) in any order. (This trades 5.5's spool-to-disk streaming for correctness; for true streaming uploads, readreq.stream()directly.)- Recursive Pydantic models registered an empty self-referential OpenAPI component; the real schema body is now emitted.
APIKeyAuth(location="query")crashed on WebSocket routes (it read.params, which a Starlette WebSocket lacks); it now falls back to.query_params.- Callable-instance generator dependencies (a class whose
__call__yields) now run their teardown instead of leaking the generator object as the value. dependency_overridesnow reaches into the app-scoped graph: overriding a dependency that anapp-scoped dependency depends on resolves correctly (was aDependencyScopeError/ stale cached value), and overrides restore per-key so nested blocks don't clobber each other.- Conditional responses. A
304now carries the negotiatedVaryheader, and aRangerequest with a matching validator returns304rather than a206body. - OpenAPI now logs a warning on a component name collision (two distinct
models sharing a
__name__) instead of silently serving one for both.
v6.0.1 - 2026-06-29
Fixed
Annotated[...]markers with aNonedefault (e.g.token: Annotated[str, Header(None)] = None) were not detected on Python 3.10, whereget_type_hintsimplicitly wraps the annotation inOptional— the marker is now found through theUnion. (Latent since 5.3.)
v6.0.0 - 2026-06-28
A small, deliberate major release: no new features — it removes the
deprecation shims introduced during 5.x and flips a few defaults to the
more-correct behavior. Every change was announced with a DeprecationWarning
or an opt-in knob during 5.x, so code that runs clean under
-W error::DeprecationWarning on 5.6 upgrades without surprises. See the
v6 migration guide.
Removed
- The case-insensitive
req.methodcomparison shim.req.methodis now a plain uppercasestr; compare against uppercase literals (req.method == "GET"). (Announced in 5.0.) - The single-unnamed-parameter dependency-provider shim. A provider's
request parameter must be named
req/requestor annotatedRequest; otherwise resolution raisesDependencyResolutionError. (Announced in 5.0.)
Changed
await req.media("files")returns streamingUploadFileobjects keyed by field name instead of a fully-buffered bytes-dict;File()markers are the typed equivalent. (Deprecated in 5.6.)- JSON defaults to
ensure_ascii=False(raw UTF-8). This changes response bytes andauto_etagvalues for non-ASCII payloads; passAPI(json_ensure_ascii=True)to restore\uXXXXescaping. (Knob added in 5.6.) Vary: Acceptis now sent by default on content-negotiated responses (correct for shared caches); passAPI(auto_vary=False)to opt out. (Opt-in added in 5.1.)Route.__hash__no longer includesbefore_request, so routes that compare equal now hash equal.
v5.6.0 - 2026-06-28
A backward-compatible release that stages the breaking changes coming in Responder 6.0 — adding the opt-in knobs and deprecation warnings so you can adapt on 5.x first. See the v6 migration guide.
Added
API(json_ensure_ascii=...)controls JSON non-ASCII escaping. The default staysTrue(escape as\uXXXX) in 5.x and flips toFalse(raw UTF-8) in 6.0; set it explicitly to lock in either behavior.
Deprecated
await req.media("files")(the fully-buffered bytes-dict) now emits aDeprecationWarning. In 6.0 it returns streamingUploadFileobjects; useFile()markers for the new API today. The bytes-dict is still returned in 5.x.- Documented the project's deprecation policy and the full 6.0 breaking-change
list in
docs/migration-v6.md.
v5.5.0 - 2026-06-28
A backward-compatible release adding type-driven file uploads and form fields, completing the typed-parameter surface. No existing call signatures change.
Added
File()andForm()markers.File()injects an uploaded file as anUploadFile(read withawait f.read(), or stream it in chunks — large uploads are spooled to disk by Starlette's parser rather than held in memory);Form()injects a form field (urlencoded or multipart), coerced and validated likeQuery(). A sequence annotation (list[UploadFile],list[str]) collects repeated fields. Both support theAnnotated[...]form.responder.UploadFileis exported for annotating upload parameters.- Multipart OpenAPI. Routes with
File()/Form()markers generate amultipart/form-data(orapplication/x-www-form-urlencoded) request body — files as{type: string, format: binary}— so the interactive docs show a file picker.
The existing await req.media("files") bytes-dict contract is unchanged.
v5.4.0 - 2026-06-28
A backward-compatible release focused on testing, operations, and HTTP correctness. No existing call signatures change.
Added
api.dependency_overrides(**overrides)— a context manager that swaps dependencies for tests and restores them on exit. Values may be bare objects (wrapped automatically) or provider callables (which still receive sub-dependencies and the request). Overrides are request-scoped, so they replace and bypass the cache of anapp-scoped dependency too.- Health checks.
api.add_health_check(name, check)registers a readiness check (sync or async; passes unless it returnsFalseor raises) andAPI(health_route="/health")serves the aggregate —200with per-check JSON when all pass,503otherwise. The route is excluded from the OpenAPI schema. - Named routes.
@api.route(..., name="...")(and the verb decorators /add_route) name a route soapi.url_for("name", **params)can reverse it by string — decoupling URL generation from the endpoint's function identity (so lambdas and shared names are addressable). Works for WebSocket routes too.
Fixed
- Conditional requests for served files.
resp.file(),resp.stream_file(), andresp.download()now set a stat-based weakETagandLast-Modifiedby default, soIf-None-Match/If-Modified-Sinceget a304(andfile()no longer reads the whole file to hash it underauto_etag). Range requests are unaffected. Passconditional=Falseto opt out.
v5.3.0 - 2026-06-28
A backward-compatible release that finishes the type-driven I/O and OpenAPI
authoring story: Annotated[] markers, generic response models, and
first-class route/operation metadata. No existing call signatures change.
Added
Annotated[]marker form.Query/Header/Cookie/Pathmarkers can now be written asq: Annotated[int, Query(ge=1)](PEP 593), keeping the parameter's default value in the usual slot — the FastAPI-familiar style — in addition to the existingq: int = Query(...)form. Constraints, aliases, and OpenAPI emission work identically either way.- Generic
response_model.response_model=list[Model], tuples, and unions (Model | ErrorModel) are now validated and serialized via aTypeAdapterand documented correctly in OpenAPI — anarraywith anitems$reffor a list,oneOf/anyOffor a union, with the referenced models hoisted intocomponents/schemas. (response_model=list[Model]previously registered a bogus schema namedlist.) A bare-> list[Model]return annotation still appears in the schema but stays un-validated at runtime, so existing handlers returning loose data keep working. - Route/operation metadata.
tags,summary,description,operation_id, anddeprecatedkwargs on@api.route(and the verb decorators) flow into the generated OpenAPI operation; a docstring-YAML block still overrides them. API(openapi_servers=[...])populates the OpenAPIserverslist.
v5.2.0 - 2026-06-28
A backward-compatible release that rounds out the security story: a batteries- included authentication extension that wires straight into OpenAPI (so the interactive docs get an Authorize button), and an opt-in security-headers middleware. No existing call signatures change.
Added
responder.ext.auth—BearerAuth,BasicAuth, andAPIKeyAuthschemes. Each is a callable that extracts the credential, runs your (sync or async)verifycallback, and returns the principal — or raises401with the correctWWW-Authenticatechallenge. Use one as a dependency to inject the principal into a handler, or call it directly. For static secrets, pass them inline (BearerAuth(tokens=[...]),APIKeyAuth(keys=[...]),BasicAuth(credentials={...})) and the scheme compares them in constant time.- OpenAPI security schemes.
api.add_security_scheme(name, scheme)(orscheme.register(api)) populatescomponents.securitySchemesso Swagger / ReDoc render an Authorize button. Asecurity=kwarg on routes (and the verb decorators) marks which operations require auth;default=Truerequires a scheme on every operation. API(security_headers=True)andresponder.middleware.SecurityHeadersMiddleware— addX-Content-Type-Options: nosniff,X-Frame-Options: DENY, andReferrer-Policy: strict-origin-when-cross-originto every response (opt-in). Pass a dict to add acontent_security_policy/permissions_policyor override any header. Headers a handler set itself are preserved.
Changed
- Dependency providers may now be callable instances with an async
__call__(previously only plain async functions were awaited; an async__call__was mistakenly run in a thread). This is what lets an auth scheme object be used directly as a dependency.
v5.1.0 - 2026-06-28
A backward-compatible follow-up that finishes what v5 started: it makes the type-driven OpenAPI and typed-parameter features correct for the common cases they missed, turns the documented-but-absent HSTS header into a real one, and adds the small ergonomics the typed surface implies. No existing call signatures change.
Fixed
- Type-driven OpenAPI now emits valid documents for nested models. A model
containing another model previously produced a dangling
#/$defs/...reference (the nested schema was never registered as a component), so Swagger UI / ReDoc / codegen could not resolve it. Each nested model is now hoisted into its own top-levelcomponents/schemasentry with rewritten$refs. - OpenAPI output matches the declared dialect. Under a
3.0.xversion,Optional[...]fields (which Pydantic v2 emits asanyOf: [..., {type: null}], invalid in 3.0) are down-converted tonullable, and array-valuedexamplesare singularized — so the document validates.3.1output is unchanged. Query()/Header()/Cookie()/Path()markers now enforce their constraints.Query(min_length=3),Query(gt=0), etc. were silently ignored (validation was built from the bare annotation); they now apply and return422on violation, and a typo'd keyword (e.g.Query(dafault=5)) now raisesTypeErrorat definition time instead of silently making the parameter required. A marker'sdescription/deprecatednow appear in the OpenAPI spec.enable_hsts=Truenow sends a realStrict-Transport-Securityheader (in addition to the existing HTTP→HTTPS redirect). It previously only redirected, despite the docs promising browsers "see the HSTS header."
Added
- HTTP verb shortcut decorators —
@api.get,@api.post,@api.put,@api.patch,@api.delete, and@api.websocket_route, plus the same on route groups. Registering@api.get("/x")and@api.post("/x")as separate handlers on one path now works (same-path routes are allowed when their methods are disjoint). resp.delete_cookie(key, ...)— expire a cookie on the client (mirrorsset_cookie'spath/domain/secure/httponly/samesite).resp.vary(*fields)— add field names to theVaryheader (merged and de-duplicated). NewAPI(auto_vary=True)emitsVary: Accepton content-negotiated responses (correct for shared caches); off by default, slated to default on in 6.0.responder.middleware.HSTSMiddleware— the HSTS header middleware, usable directly viaadd_middlewareto customizemax_age/preload.
v5.0.0 - 2026-06-28
A major release: fully type-driven request/response I/O, composable dependency
injection, plan-driven OpenAPI, secure-by-default sessions, and a deferred
middleware stack — layered onto the unchanged (req, resp) core. Breaking
changes are staged behind the v5 migration guide.
Added
- Type-hint-driven OpenAPI. The schema is now generated from each route's
methods, body/response models, and
Query/Header/Cookiemarkers — so routes without a YAML docstring now appear in the spec (with parameters, request/response schemas, and an automatic422for validating routes). Docstring YAML is deep-merged on top as an override. New@api.route(..., include_in_schema=False)hides a route; internal routes (schema/docs/static/metrics) are auto-excluded. - Typed parameter markers.
Query(),Header(),Cookie(), andPath()(exported fromresponder) inject validated, type-coerced query parameters, headers, cookies, and path parameters as handler arguments —def search(req, resp, *, q: str = Query(...), limit: int = Query(10))— with422on a missing required value or a validation failure. Supports defaults, aliases, and sequence types (list[int]from repeated query keys). - Return annotation as
response_model. A Pydantic return annotation (def handler(req, resp) -> ItemOut) now validates/serializes the response against that model (coerce types, strip undeclared fields), the same as an explicitresponse_model=. - Composable dependency injection. A dependency provider can now depend on
other providers by declaring them as parameters — resolved recursively,
memoized across the whole request graph, torn down in reverse-topological
order, with cycle detection. App-scoped dependencies may compose with other
app-scoped ones (but never with the request or request-scoped deps).
Providers receive the request only via a
req/requestparameter or aRequest/WebSocketannotation (a sole-unnamed-param shim warns for one cycle). NewDependencyError/DependencyCycleError/DependencyScopeError/DependencyResolutionErrorexceptions;req/request/resp/response/ws/websocketare reserved dependency names. - Async-native backends. Session and rate-limit backends may now expose
async methods (
aget/aset/adelete/atouch,ahit) that are awaited directly instead of run in a thread — withAsyncRedisSessionBackendandAsyncRedisBackendbuilt in (redis.asyncio).RateLimiter.acheck()and.install()work with any backend. BackendProtocols are exported for typing. - Sliding server-side session TTL via
touch. An unchanged read-only request now refreshes the backend TTL withtouch/atouch(no full re-serialize) while a mutation still does a fullset— fixing the premature-logout/dirty-tracking trade-off correctly. api.add_exception_handler(exc_or_status, handler)registers an error handler programmatically (the@api.exception_handlerdecorator now delegates to it). A handler for500/Exceptioninstalls the catch-all server-error handler.api.add_middleware()now works after construction — middleware is collected and the stack is built lazily on first request.
Security
- Secure-by-default sessions. The session signing key no longer defaults to
the public
"NOTASECRET". Withsessions="auto"(the default) Responder uses yoursecret_key/RESPONDER_SECRET_KEY, else mints a random per-process key with a loud warning (set a key for stable, multi-worker sessions).secret_key="NOTASECRET"now raises. Session cookies areSecureby default in production (session_https_only=None), andSameSite=Nonewithout Secure is rejected. sessions=andsession_max_age=knobs.sessions=Truerequires a real key (raises otherwise);sessions=Falsedisables session middleware entirely and makesreq.session/resp.sessionraise a guidingRuntimeError.
Changed
resp.sessionis now a property that delegates toreq.session(soresp.session = {...}actually writes through to the cookie). Reading either with sessions disabled raisesRuntimeError.- Deferred middleware stack. The ASGI stack is assembled lazily with
ServerErrorMiddlewareas the outermost application layer (so it catches errors from any middleware — sessions, CORS, user middleware) while the logging/request-id tier wraps even it, soX-Request-IDand the real status now appear on500responses.API.appis a lazy read-only property; mutate the stack viaadd_middleware()or wrap the API object for a truly-outermost layer. User middleware now sits insideServerErrorMiddleware(its errors are caught), and sessions sit below it (a write is not persisted on an unhandled500). req.methodis now UPPERCASE ("GET"not"get"), matching Flask/FastAPI/Starlette/stdlib. For one deprecation cycle it returns anHTTPMethod(astrsubclass) that still compares case-insensitively — soreq.method == "get"keeps working but emits aDeprecationWarning. Hash- based membership (req.method in {"get"}) is case-sensitive and misses; use==, a tuple/list, or uppercase keys. Removed in Responder 6.0.
v4.1.0 - 2026-06-28
A backward-compatible quality release: verified correctness and resource-safety fixes, additive security hardening, and developer-experience improvements. No existing call signatures change.
Added
responder.types: publicHandler,Hook, andDependencytype aliases for annotating your own handlers and hooks.- The
req/resphot surface (headers,params,method,cookies,mimetype,is_json,is_secure, …) and the dynamicstatus_codesconstants are now statically typed, so the shippedpy.typedmarker actually helps downstream type checkers (and removes internaltype: ignores). - Type-hint-driven body injection: a handler parameter annotated with a
Pydantic model now receives the validated request body —
async def create(req, resp, *, item: ItemIn)— returning422on invalid input. Works for sync and async handlers and coexists with path parameters and dependencies. (Previously such a signature raised a500.) Use an explicitresponse_model=for response validation. - First-class Pydantic models:
resp.media = modelandreturn Model()now serialize correctly (across JSON, YAML, and MessagePack), as do dataclasses. The JSON encoder also handlesdatetime,date,time,UUID,Decimal,set, andbytesnatively —resp.media = {"created_at": datetime.now()}no longer 500s. - Pluggable type encoder:
API(encoder=...)accepts anobj -> serializablecallable applied across all response formats (JSON, YAML, MessagePack) to serialize custom types. It's tried first and falls back to the built-in conversions. - Flask-style tuple returns: a handler may
return body, statusorreturn body, status, headers(previously these were silently dropped). responder.abort(status_code, *, detail=None, headers=None)raises a rendered HTTP error from anywhere in a handler or dependency, without importing Starlette.- Bare lifecycle decorators:
@api.before_requestand@api.after_requestnow work without the parentheses, alongside the existing called forms. - Sync body access:
req.media_sync()andreq.text_synclet synchronous handlers read the request body (they bridge to the loop from the worker thread Responder already runs sync handlers in).
Changed
response_modelnow fails closed instead of leaking. Previously a response whose data didn't satisfy its declaredresponse_modelwas sent through unvalidated (wrong types, undeclared fields and all). It now coerces and strips as documented, and on a genuine validation failure raises in debug mode or returns a500(never the unvalidated payload) in production. Valid responses are unaffected;datetime/UUID/etc. fields now serialize instead of 500-ing. List responses are validated item-by-item.- Malformed request bodies now return 400 instead of 500: invalid
JSON, YAML, or MessagePack raise a rendered
400, and binary parts in areq.media("form")call are skipped rather than crashing. API(auto_escape=...)is now actually forwarded to the template environment (it was previously ignored).resp.okreads as200-based success until a status code is set, instead of raising.- Per-request overhead trimmed: coroutine-function detection for views and
hooks is memoized, and
req.paramsreads the raw query string from the ASGI scope instead of rebuilding and re-parsing the full URL.
Fixed
- Mounted-app routing: a mount prefix now only matches on a path-segment
boundary, so e.g.
GET /subscribeis no longer mis-routed into an app mounted at/sub(and sub-paths keep their leading slash). More specific (longer) mount prefixes resolve first. - Dependency teardown leaks: a failing generator-dependency teardown no longer skips the remaining teardowns — each runs best-effort and failures are logged, so connections/files/locks are always released. Applies to HTTP routes, WebSocket routes, and app-scoped shutdown.
- Event-loop blocking: server-side session backends (e.g. Redis) now run
their
get/set/deleteoff the event loop,resp.file()reads file bytes in a worker thread, and the staticindex.htmlfallback reads off the loop too — so these no longer stall the server fromasynchandlers. - Request-size enforcement:
max_request_sizeis now enforced while the body streams in (chunked or lyingContent-Length) instead of buffering the whole body first; also fixes an empty-body re-read. BackgroundQueue.__call__no longer wraps work in a pointless create-task-then-await; its await-to-completion semantics are now documented (use.task/.runfor fire-and-forget).
Security
- Default secret key warning: Responder now logs a loud warning when cookie
sessions are signed with the built-in
"NOTASECRET"default key (outside debug mode) — forged session data is otherwise trivial. SetAPI(secret_key=...). - Session fixation defense: server-side sessions mint a fresh id when a
presented cookie doesn't resolve, and a new
regenerate_session(req)helper (responder.ext.sessions) rotates the id after login/privilege change. - Session cookie controls:
API(session_cookie=..., session_https_only=..., session_same_site=...)configure the session cookie for both cookie-payload and server-side sessions. - GraphQL hardening:
api.graphql(..., graphiql=..., introspection=..., max_depth=...)can disable the in-browser IDE, reject schema-introspection queries, and cap query nesting depth (a DoS guard) for production. - Path-traversal jail:
resp.file(path, root=...),resp.stream_file(..., root=...), andresp.download(..., root=...)resolvepathunderrootand return404on any../symlink escape — use whenever the path is user input. - Open-redirect guard:
resp.redirect(location, allow_external=False)(andapi.redirect(...)) refuse to redirect to an absolute or protocol-relative URL — use for user-supplied locations.
v4.0.0 - 2026-06-12
Changed
- Breaking: Slimmed the default install from ~60 packages to ~30 by
moving heavy dependencies behind extras:
pueblo[sfa-full](which pulls ins3fs,aiobotocore,aiohttp,libarchive-c, and friends) is now thecliextra.responder runstill works out of the box for local modules and file paths (app:api,myapp/core.py); only remote targets (URLs,github://, cloud storage) needpip install 'responder[cli]'grapheneandgraphql-coreare now thegraphqlextra. Install withpip install 'responder[graphql]'to useapi.graphql()
v3.12.0 - 2026-06-12
Added
- Built-in metrics:
API(metrics_route="/metrics")serves request counts and latency histograms in Prometheus text format, zero dependencies. Labels use route patterns (/users/{id}) so cardinality stays bounded; error responses are recorded with their real status codes - Server-side sessions:
API(session_backend=...)stores session data in a backend (MemorySessionBackend,RedisSessionBackend, or any object withget/set/delete) with only an opaque ID in the cookie — enabling revocation and unbounded session size. Handler code is unchanged - Query-parameter validation:
@api.route(..., params_model=Model)coerces and validates query strings with Pydantic (422on failure), exposes the instance asreq.state.validated_params, maps repeated keys tolistfields, and documents the parameters in the OpenAPI spec resp.render(template, **context)— render a Jinja2 template as the HTML response body in one call
v3.11.0 - 2026-06-11
Added
- HTTP range requests:
resp.file()andresp.stream_file()answerRange: bytes=...with206 Partial Content(suffix and open-ended ranges,416for unsatisfiable,Accept-Rangesadvertised) — enables video seeking and resumable downloads resp.download(path, filename=...)serves files as attachments with properContent-Disposition(RFC 5987 encoding for non-ASCII names), streamed and resumable- Request timeouts:
API(request_timeout=seconds)answers overrunning handlers with504 Gateway Timeout(content-negotiated); dependency teardowns still run
Performance
- Route resolution is cached per (method, path) with invalidation on registration — ~10% faster dispatch at 81 routes, growing with route count
v3.10.0 - 2026-06-11
Added
- Trailing-slash redirects: requests that miss only by a trailing slash get
a
307to the canonical path, preserving method and query string. Disable withAPI(redirect_slashes=False) - Request size limits:
API(max_request_size=bytes)returns413for oversized bodies — fast-fails onContent-Lengthand enforces cumulatively for chunked/streamed uploads - Automatic ETags:
API(auto_etag=True)adds a content-hashETagto GET responses with full304 Not Modifiedhandling; an explicitresp.etagalways wins - After-response background tasks:
resp.background(func, *args)defers work until the client has the response (sync and async, ordered) resp.cache_control(...)helper for buildingCache-Controlheaders
Fixed
- A
413raised while reading the body duringrequest_modelvalidation is no longer swallowed into a422
Changed
- Trailing-slash redirects are on by default (previously such requests were 404s)
v3.9.1 - 2026-06-11
Added
- Conditional request support: set
resp.etagorresp.last_modifiedand matchingIf-None-Match/If-Modified-Sincerequests automatically get304 Not Modified(RFC 7232 semantics:If-None-Matchprecedence, weak comparison, GET/HEAD only) - Request body streaming:
async for chunk in req.stream()iterates large uploads without buffering - Pluggable rate-limiter backends:
RateLimiter(backend=...)with the in-memory default plus a newRedisBackendfor multi-process deployments - Application state:
api.statenamespace, reachable from handlers viareq.api.state req.apiis now populated with the owningAPIinstance (it was alwaysNonebefore)
Fixed
API(static_dir=None)crashed on every route registration — the static fallback assertion now only applies when no endpoint is given- The static-fallback error is a
ValueErrorinstead of a bareassert
Performance
- Request headers are parsed into the case-insensitive dict lazily, on first access (~5% faster dispatch on header-heavy requests that don't read headers)
v3.9.0 - 2026-06-11
Added
- Dependency injection for WebSocket handlers: declare path parameters and
registered dependencies by name after the
wsargument. Request-scoped providers taking a parameter receive the WebSocket; generator teardown runs when the handler finishes. Handlers that only takewsare unaffected - OpenAPI 3.1 support (
openapi="3.1.0") - The OpenAPI schema endpoint now serves JSON when requested via
Accept: application/json, or always whenopenapi_routeends in.json - Path parameters are documented automatically in the OpenAPI spec from
route patterns (
{id:int}→ required integer parameter) - Built-in error responses (404, 405) are content-negotiated: JSON clients
receive
{"error": ...}bodies instead of plain text
Fixed
- OpenAPI paths no longer leak convertor patterns (
/users/{id:int}is now emitted as the spec-compliant/users/{id}) - Registering a duplicate route now raises
ValueError(previously anassertthat disappears underpython -O) - Removed dead
_exception_handlersbookkeeping inAPI.exception_handler
Changed
mypynow passes with zero errors across the codebase (was 25);ruffis clean as welltypes-pyyamladded to thetestextra
v3.8.0 - 2026-06-11
Added
- Handlers can return values: a
dict/listsetsresp.media, astrsetsresp.text, andbytessetresp.content. ReturningNonekeeps the mutate-respbehavior, so existing handlers are unaffected - App-scoped dependencies:
@api.dependency(scope="app")resolves the provider once on first use and caches it for the application's lifetime; generator teardown runs at shutdown - Automatic
OPTIONSresponses with anAllowheader for method-restricted routes HEADrequests are accepted whereverGETisset_cookie()gains asamesiteparameter, defaulting to"lax"- The validated
request_modelinstance is now available to handlers asreq.state.validated— no need to re-parse the body
Changed
- Requests to an existing path with an unsupported method now return
405 Method Not Allowedwith anAllowheader (previously 404) RouteGroup.before_requesthooks are now scoped to the group's prefix (previously they silently applied to every route)
Performance
- View signature inspection for dependency injection is cached per function
v3.7.0 - 2026-06-11
Added
- Dependency injection for route handlers: register providers with
@api.dependency()(orapi.add_dependency(name, provider)) and declare them as view parameters by name. Supports sync/async functions and generators (code afteryieldruns as teardown once the response is sent). Providers accepting a parameter receive the currentRequest. Dependencies resolve at most once per request; path parameters take precedence. - Per-route rate limiting via
RateLimiter.limitdecorator - WebSocket before-request hooks can now reject connections: closing the socket in a hook short-circuits the route handler
- WebSocket before-request hooks may now be sync functions (run in the threadpool)
- Custom formats registered on
api.formatsnow actually reach request parsing and response negotiation (previously each request got a fresh default format registry)
Fixed
{value:float}path convertor matched garbage like1a5(unescaped regex dot) and crashed with a 500 — now correctly returns 404- Literal characters in route paths are now regex-escaped, so
/file.jsonno longer matches/fileXjson - Unbounded memory growth in
BackgroundQueue— completed futures are now pruned fromresults req.media("form")crashed with aTypeErrorwhen the request had noContent-Typeheader- Content negotiation returned an empty body for
Acceptheaders matching encode-incapable formats (e.g.multipart/form-data) — now falls through to JSON
Changed
Request.urlandRequest.paramsare now computed once and cached- Format registries are no longer rebuilt twice per request
v3.6.2 - 2026-04-12
Fixed
- GraphQL error responses now correctly return 400 status instead of always 200
- OpenAPI docs UI now respects custom
openapi_routeinstead of hardcoding/schema.yml before_requestsdefault type mismatch that could crash routes called outside the router- Blocking synchronous file I/O in
Response.stream_file()— now uses async I/O via anyio - Memory leak in rate limiter (empty bucket keys never cleaned up)
- Race condition in rate limiter
check()— added thread-safe locking - WSGI fallback catching all
TypeErrors instead of just call-signature mismatches - Pydantic request/response model validation crashing on non-dict bodies
- Test assertions that could never fail (
or True,< 500patterns) CaseInsensitiveDictmissing__delitem__,pop, andsetdefaultoverridesassertused for input validation in OpenAPI extension (stripped bypython -O)- Potential XSS in GraphiQL template endpoint injection
- Dead
or ""in media format detection logic
Changed
DELETErequests now participate in Pydantic request body validation- Simplified status code category check to use chained comparison
Removed
- Unused
methodparameter fromload_target() - Unused Node.js setup step from CI test workflow
v3.6.1 - 2026-04-12
Added
- Configurable GZip compression via
gzipparameter onAPI()(defaults toTrue)
v3.6.0 - 2026-03-24
Added
- Built-in structured logging with per-request context (
enable_logging=True)api.log— always-available logger, enriched with request context when logging is enabled- Automatic access logging with timing:
GET /path → 200 (1.2ms) - Request ID generation/forwarding via
X-Request-IDheader contextvars-based request context (ID, method, path, client IP) on every log recordresponder.ext.loggingmodule:get_logger(),RequestContext,RequestContextFilter
- CLAUDE.md project guide and
/releasecommand - Version number in docs sidebar
Changed
- Comprehensive documentation improvements across all pages
- Deployment: health checks, Docker Compose, Caddy, Procfile, production checklist
- API reference: usage examples for every class
- Feature tour: Pydantic validation, content negotiation, structured logging sections
- Tutorials: modernized SQLAlchemy to
mapped_column(), fixed deprecateddatetime.utcnow(), WebSocketWebSocketDisconnecthandling, role-based auth, auth strategy guide - Testing: rate limiting and WSGI mount examples
- Middleware: pure ASGI middleware example
- Quickstart: links to all tutorials
- Sandbox: full rewrite with project layout
- Docker example uses
uvinstead of pip - Backlog updated: removed implemented features, replaced HTTP/2 server push with dependency injection
Removed
uv.lock— this is a library, not an application
v3.5.0 - 2026-03-24
Added
- CI validation for Python 3.14, 3.14 free-threaded, and PyPy 3.11
- Marimo notebook mounting docs and example
- Type annotations for
routes.py
Changed
- Replaced deprecated
asyncio.iscoroutinefunctionwithinspect.iscoroutinefunctionahead of Python 3.16 removal - Narrowed broad
except Exceptionto specific exceptions in response model serialization and websocket chat example - Improved GraphQL API interface with expanded test coverage
- Code formatting cleanup via pyproject-fmt and ruff
- Dropped Python 3.9 from CI
Fixed
- WSGI mount returning 400 when requesting the exact mount root path
- Werkzeug 3.1.7 compatibility for trusted host validation in tests
future.resultbare property access in background task test (now properly callsfuture.result())- OpenAPI template packaging and static file serving
- RST title underline warning breaking docs CI
Removed
- Read the Docs configuration (docs hosted on GitHub Pages)
v3.4.0 - 2026-03-22
Changed
- Upgraded to Starlette 1.0
- Added comprehensive docstrings across the codebase
- Expanded API reference documentation
v3.3.0 - 2026-03-22
Added
- Full documentation rewrite: tutorials for REST APIs, SQLAlchemy, Flask migration
- Auth, WebSocket, middleware, and configuration guides
- Testing docs with prose, examples, and tips
- GitHub Pages deployment for docs
Changed
- Reworked homepage prose
- Rewrote CLI and API reference docs
v3.2.0 - 2026-03-22
Added
- Pydantic auto-validation:
request_modelvalidates input, returns 422 on failure - Pydantic auto-serialization:
response_modelstrips extra fields from responses - Server-Sent Events:
@resp.ssefor real-time streaming resp.stream_file()for streaming large files without loading into memory@api.after_request()hooksapi.group("/prefix")for route groups and API versioningapi.graphql("/path", schema=schema)one-liner GraphQL setupapi = responder.API(request_id=True)for automatic request ID generation- Built-in rate limiter:
RateLimiter(requests=100, period=60).install(api) - MessagePack format support:
await req.media("msgpack") req.is_json,req.path_params,req.clientpropertiesapi.exception_handler()decorator for custom error handling- Lifespan context manager support
uuidandpathroute convertors- PEP 561
py.typedmarker - Pydantic support for OpenAPI schema generation
Changed
- Dependencies flattened:
pip install respondergets everything - Core deps reduced to starlette + uvicorn
- TestClient lazy-loaded (no httpx import in production)
- Before-request hooks can short-circuit by setting status code
- Removed poethepoet task runner
Fixed
- Multipart parser losing headers when parts have multiple headers
url_for()with typed route params ({id:int})resp.bodyencoding crash on bytes content- GraphQL text query missing
await - Streaming responses not sending Content-Type headers
- Python 3.9 compatibility for union type syntax
v3.0.0 - 2026-03-22
Added
- Platform: Added support for Python 3.10 - Python 3.13
- CLI:
responder runnow also accepts a filesystem path on its<target>argument, enabling usage on single-file applications. - CLI:
responder runnow also accepts URLs.
Changed
- Platform: Minimum Python version is now 3.9 (dropped 3.6, 3.7, 3.8)
- Dependencies: Dramatically reduced core dependency count (10 → 5)
- Removed
requests,requests-toolbelt,rfc3986,whitenoise - Moved
apispecandmarshmallowtoopenapioptional extra - Replaced
rfc3986with stdliburllib.parse - Replaced
requests-toolbeltmultipart decoder withpython-multipart - Replaced deprecated
starlette.middleware.wsgiwitha2wsgi - Switched from WhiteNoise to ServeStatic
- Removed
- Dependencies: Pinned
starlette[full]>=0.40(was unpinned) - GraphQL: Upgraded to
graphene>=3andgraphql-core>=3.1(fromgraphene<3andgraphql-server-core, which is unmaintained) - GraphQL: Updated GraphiQL UI from 0.12.0 (2018) to 3.0.6 with React 18
- Extensions: All of CLI-, GraphQL-, and OpenAPI-Support modules are
extensions now, found within the
responder.extmodule namespace. - Packaging: Migrated from
setup.pyto declarativepyproject.toml
Removed
- Platform: Removed support for EOL Python 3.6, 3.7, 3.8
- Status codes: Removed deprecated
resume_incompleteandresumealiases for HTTP 308 (marked for removal in 3.0) - CLI:
responder run --buildceased to exist
Fixed
- Routing: Fixed dispatching
static_route=Noneon Windows - uvicorn:
--debugnow maps to uvicorn'slog_level = "debug" - Tests: Fixed deprecated httpx TestClient usage
v2.0.5 - 2019-12-15
Added
- Update requirements to support python 3.8
v2.0.4 - 2019-11-19
Fixed
- Fix static app resolving
v2.0.3 - 2019-09-20
Fixed
- Fix template conflicts
v2.0.2 - 2019-09-20
Fixed
- Fix template conflicts
v2.0.1 - 2019-09-20
Fixed
- Fix template import
v2.0.0 - 2019-09-19
Changed
- Refactor Router and Schema
v1.3.2 - 2019-08-15
Added
- ASGI 3 support
- CI tests for python 3.8-dev
- Now requests have
statea mapping object
Deprecated
- ASGI 2
v1.3.1 - 2019-04-28
Added
- Route params Converters
- Add search for documentation pages
Changed
- Bump dependencies
v1.3.0 - 2019-02-22
Fixed
- Versioning issue
- Multiple cookies.
- Whitenoise returns not found.
- Other bugfixes.
Added
- Stream support via
resp.stream. - Cookie directives via
resp.set_cookie. - Add
resp.htmlto send HTML. - Other improvements.
v1.1.3 - 2019-01-12
Changed
- Refactor
_route_for
Fixed
- Resolve startup/shutdwown events
v1.2.0 - 2018-12-29
Added
- Documentations
Changed
- Use Starlette's LifeSpan middleware
- Update denpendencies
Fixed
- Fix route.is_class_based
- Fix test_500
- Typos
v1.1.2 - 2018-11-11
Fixed
- Minor fixes for Open API
- Typos
v1.1.1 - 2018-10-29
Changed
- Run sync views in a threadpoolexecutor.
v1.1.0 - 2018-10-27
Added
- Support for
before_request.
v1.0.5- 2018-10-27
Fixed
- Fix sessions.
v1.0.4 - 2018-10-27
Fixed
- Potential bufix for cookies.
v1.0.3 - 2018-10-27
Fixed
- Bugfix for redirects.
v1.0.2 - 2018-10-27
Changed
- Improvement for static file hosting.
v1.0.1 - 2018-10-26
Changed
- Improve cors configuration settings.
v1.0.0 - 2018-10-26
Changed
- Move GraphQL support into a built-in plugin.
v0.3.3 - 2018-10-25
Added
- CORS support
Changed
- Improved exceptions.
v0.3.2 - 2018-10-25
Changed
- Subtle improvements.
v0.3.1 - 2018-10-24
Fixed
- Packaging fix.
v0.3.0 - 2018-10-24
Changed
- Interactive Documentation endpoint.
- Minor improvements.
v0.2.3 - 2018-10-24
Changed
- Overall improvements.
v0.2.2 - 2018-10-23
Added
- Show traceback info when background tasks raise exceptions.
v0.2.1 - 2018-10-23
Added
- api.requests.
v0.2.0 - 2018-10-22
Added
- WebSocket support.
v0.1.6 - 2018-10-20
Added
- 500 support.
v0.1.5 - 2018-10-20
Added
- File upload support
Changed
- Improvements to sequential media reading.
v0.1.4 - 2018-10-19
Fixed
- Stability.
v0.1.3 - 2018-10-18
Added
- Sessions support.
v0.1.2 - 2018-10-18
Added
- Cookies support.
v0.1.1 - 2018-10-17
Changed
- Default routes.
v0.1.0 - 2018-10-17
Added
- Prototype of static application support.
v0.0.10 - 2018-10-17
Fixed
- Bugfix for async class-based views.
v0.0.9 - 2018-10-17
Fixed
- Bugfix for async class-based views.
v0.0.8 - 2018-10-17
Added
- GraphiQL Support.
Changed
- Improvement to route selection.
v0.0.7 - 2018-10-16
Changed
- Immutable Request object.
v0.0.6 - 2018-10-16
Added
- Ability to mount WSGI apps.
- Supply content-type when serving up the schema.
v0.0.5 - 2018-10-15
Added
- OpenAPI Schema support.
- Safe load/dump yaml.
v0.0.4 - 2018-10-15
Added
- Asynchronous support for data uploads.
Fixed
- Bug fixes.
v0.0.3 - 2018-10-13
Fixed
- Bug fixes.
v0.0.2 - 2018-10-13
Changed
- Switch to ASGI/Starlette.
v0.0.1 - 2018-10-12
Added
- Conception!