Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
85 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.
Unreleased
v8.0.0 - 2026-07-01
Added
- Standalone router composition:
responder.Routerrecords route declarations without anAPIinstance — like Flask blueprints or FastAPI'sAPIRouter— andapi.include_router(router, prefix=...)attaches them. Routers nest (prefixes compose, tags merge, dependencies concatenate), carry group-leveltags/dependencies/authdefaults that individual routes may override, scope theirbefore_requesthooks to the mounted prefix, and can be included at more than one prefix. Declarations are replayed throughapi.route(), so auth inheritance,Dependsguards, and OpenAPI metadata work exactly as if declared on the API directly. Including a view with auth/tags/dependencies that conflict with an earlier registration of the same function raisesValueErrorinstead of silently rewriting the earlier route (an access-control hazard otherwise).api.group()remains for quick same-file prefix grouping.
Changed
- Text responses now declare their charset the standard way —
Content-Type: text/...; charset=<encoding>— andresp.encodingis honored when encodingstrbodies for all text types (previouslyresp.htmlignored it and str bodies fell through to a UTF-8 encode, producing mojibake for e.g.latin-1). The charset parameter is only added when the framework actually encodes astrbody; rawbytesbodies (e.g.resp.file()) keep their bare media type. The nonstandardEncodingresponse header is no longer sent. resp.redirect()(and theapi.redirect()helper) now default to a method-preserving307 Temporary Redirectinstead of301 Moved Permanently, which browsers cache indefinitely and legacy clients rewrite POST→GET. Passpermanent=Truefor a308 Permanent Redirect; an explicitstatus_code=still takes precedence.- YAML responses are served as
application/yaml(the RFC 9512 registered media type) instead of the legacyapplication/x-yaml; the OpenAPI/schema.ymlroute follows suit. Inbound requests are unaffected — bodies sent with either media type still parse as YAML, andAccept: application/x-yamlstill negotiates a YAML response. - An empty or whitespace-only YAML request body now raises
400 Bad Requestfromreq.media(), matching the JSON format (previouslyyaml.safe_loadsilently returnedNone). An explicit YAMLnulldocument still parses toNone. url_fornow raisesRouteNotFoundError(aLookupErrorsubclass, importable asresponder.RouteNotFoundError) for an unknown endpoint or route name instead of returningNone(which rendered a literal "None" in templates), and generated URLs percent-encode parameter values —url_for(view, name="a b")yields/file/a%20b;{param:path}segments keep their slashes.- Requests into a WSGI mount no longer build a fresh
a2wsgi.WSGIMiddleware(and its per-instanceThreadPoolExecutor) on every request; the app is wrapped once, at mount time. API()no longer createsstatic_dirimplicitly. Merely instantiatingAPI()used tomkdiran emptystatic/into the working directory (and failed on read-only filesystems); the defaultstatic/is now mounted only when the directory already exists, and an explicitly passedstatic_dirthat doesn't exist raisesFileNotFoundErrorat construction.static_dir=Nonestill disables static serving, and the docstrings no longer claimstatic_dir/templates_dirare "created for you".
Fixed
Request.cookiesno longer silently drops cookies: theCookieheader is now parsed with Starlette's tolerant parser instead ofhttp.cookies.SimpleCookie, which aborted at the first nonconforming token (e.g.b[]=2) and discarded that cookie and every one after it.Response.headersis now a case-insensitive, case-preserving mapping, so a handler settingresp.headers['content-type']replaces the framework'sContent-Typeinstead of emitting both headers on the wire (andno_content()no longer needs to pop both spellings).CaseInsensitiveDictconstruction from a mapping,|=/|,.copy(), and.fromkeys()are also case-insensitive now — each previously bypassed the lowercasing — iteration preserves the casing keys were set with, and instances survivepickle,copy.copy, andcopy.deepcopyround-trips.- Background tasks now support async functions:
@api.background.taskandapi.background.rundrive anasync defto completion viaasyncio.runon the worker thread (afunctools.partialwrapping one works too). Previously an async task silently produced a never-awaited coroutine and the job never ran. Background worker threads are also now namedresponder-background-*for easier debugging. Sync task behavior is unchanged. - Assigning
templates.context = {...}no longer wipes Jinja2's built-in globals (range,namespace,cycler,joiner,lipsum,dict) — previously any template usingrange()after a context assignment raisedUndefinedError. The setter now updates the globals dict in place (keeping the sync and async environments sharing one globals object), and reassigning the context restores any built-in a previous assignment had shadowed. - The GraphQL view now executes queries with
schema.execute_async, soasync defresolvers (first-class in graphene 3) resolve correctly instead of failing with a "coroutine was never awaited" error, and resolvers can yield the event loop while they await. The query document is also parsed once and shared by the GET-mutation guard and introspection/depth validation instead of being re-parsed by each. API(lifespan=...)no longer silently disableson_eventhandlers: startup events run after the lifespan context manager is entered and shutdown events before it exits, so background-task draining (registered as a shutdown event) is no longer skipped. A raising lifespan__aexit__now still tears down app-scoped dependencies and reportslifespan.shutdown.failed.add_route(default=True)with a Responder(req, resp)view no longer crashes every unmatched request (the default endpoint was invoked ASGI-style, raisingTypeError→ 500). A view-style default is now dispatched through normal route semantics — before/after hooks, dependency injection, and response handling — with empty path params; ASGI-style defaults (including the built-in 404) are unchanged.HEADrequests to a class-based view that defines onlyon_getnow fall back toon_get(with the response body stripped) instead of returning 405, matching the implicit HEAD support function views already had.- A mount prefix with a trailing slash (
api.mount("/admin/", app)) now matches subpaths; the prefix is normalized at mount time, and the empty-prefix root mount still catches everything. - An unhandled exception in a WebSocket handler now closes the socket with code 1011 (internal error) before re-raising, so clients can tell a server bug from a network drop instead of seeing an abnormal 1006 with no close frame.
- Two distinct dependency providers that share a
__name__(two functions both namedget_db, or nested inline lambdaDepends) no longer trip a falseDependencyCycleError— cycle detection now tracks provider identity, keeping names only for the error message. url_forno longer raisesAttributeErrorwhen any registered route uses a callable-instance endpoint without a__name__(e.g. a mountedGraphQLView); the by-name fallback scan now skips such endpoints.api.group(...).route(None)now raises the usual "a route path is required" error instead of silently registering a literal/prefixNonepath built from the f-string.jinja2,pyyaml, anditsdangerousare now declared as direct dependencies. Responder imports them directly (templates, YAML content negotiation, and cookie-based session middleware) but previously received them only transitively via thestarlette[full]extra.python-multipartnow carries a>=0.0.12floor (the first release that ships thepython_multipartmodule name Responder imports), theapispecandmarshmallowfloors were raised to>=6.6and>=3.20to match what the OpenAPI extension is tested against, and the build system now requiressetuptools>=77, the first version that supports the PEP 639 SPDXlicense = "Apache-2.0"expression (the previous>=42pin could not build the project).
Security
resp.download()sanitizes user-derived filenames inContent-Disposition: CR, LF, and NUL are stripped (they allowed header injection),"and\are backslash-escaped in the quoted-string (a quote could break out offilename="..."), and names containing characters outside the HTTP token set now also carry the RFC 5987filename*=form (non-ASCII names keep thefilename*=-only form).- The in-memory
MemorySessionBackendnow bounds its store (LRU eviction,max_keys, default 100,000) and opportunistically sweeps expired entries on writes, so an attacker rotating session cookies — or plain abandoned sessions, previously deleted only on aget()with that exact ID — can no longer grow process memory without bound. Eviction removes earliest-expiry entries first, and operations are lock-guarded, as they run on threadpool workers.
v7.3.0 - 2026-07-01
Added
- Inline
Depends(...)can now nest inside a provider (sub-dependency chains), matching FastAPI. Previously only named@api.dependency()providers could chain; a provider with an inlineDependsparameter raisedDependencyResolutionError. - A developer
Makefile: baremakelists tasks (test,lint,types,docs,build,check, …), all run throughuv.make testandmake docsdepend onmake uv-sync.
Changed
Request.accepts()now honorsAcceptmedia ranges (*/*,type/*) and q-values (aq=0range is not acceptable) instead of substring-matching the raw header. An absentAcceptaccepts anything.- Request body decoding honors the standard
Content-Typecharset=parameter, using chardet only when no charset is declared. Request.media()auto-detects MessagePack (application/x-msgpack) bodies.- Dependency and request-body-model signature inspection is now memoized on the
per-request path, and large
auto_etagresponse bodies are hashed off the event loop. - The project
HomepageURL now points at the documentation site.
Fixed
- Multipart form parsing no longer emits file parts (or their filenames) as phantom form fields, and correctly parses unquoted field names — it now uses a real Content-Disposition parser.
enable_loggingno longer drops a request whoseX-Request-IDis not valid UTF-8; the header is decoded as latin-1, matchingRequestIDMiddleware.- The
responder.ext.querydescending sort keepsNonevalues last, per its documented contract (it previously pushed them to the front). - Mounted sub-app WSGI-vs-ASGI detection is decided from the call signature
rather than the text of a raised
TypeError, so a genuineTypeErrorinside a mounted ASGI app keeps its real traceback instead of being misclassified. - A user
lifespan(app)context manager now receives theAPIinstance instead ofNone. MetricsCollectorreads/writes are lock-guarded, so a concurrent/metricsscrape no longer risksRuntimeError: dictionary changed size during iterationand counts stay exact on free-threaded CPython.Templates.render_asyncuses a dedicated always-async Jinja environment instead of togglingis_asyncon a shared one, fixing a race between concurrent sync and async renders.
Security
- The in-memory rate-limit
MemoryBackendnow bounds its key set (LRU eviction,max_keys), so a client rotating source IPs (or spoofingX-Forwarded-For) can no longer grow process memory without bound. - The Server-Sent Events heartbeat queue is now bounded, restoring producer backpressure so a slow/stalled client can't make it buffer without limit.
v7.2.1 - 2026-07-01
Security
- The GraphQL view now rejects mutations sent over HTTP
GET(returns405withAllow: POST).GETmust be safe/idempotent; running a mutation viaGET /graphql?query=mutation{...}made it CSRF-able (a plain<img src>would trigger it with the victim's cookies) and cacheable. Query operations overGETare unaffected.
Fixed
- The client generator now escapes path-parameter names when interpolating them
into generated string literals (Python
repr, JS via the JS escaper, Ruby via a new single-quoted escaper). A name containing a quote or backslash in an (untrusted) OpenAPI spec previously produced a client that failed to parse.
v7.2.0 - 2026-07-01
Security
- Server-Sent Events frames can no longer be spoofed via caller-supplied field
values. A
\ror\nin an SSEevent,id,retry, or comment field is now stripped (these are single-line fields; a line break would terminate the field and inject additional SSE lines/frames), anddatasplits on any line terminator (\r\n,\r,\n) so a lone\rcan't inject a new field.
Added
API(ws_idle_timeout=...)closes a WebSocket that waits too long for the next inbound message (close code1001). The deadline resets on every message, so it bounds idle time between messages, not total connection lifetime. Defaults toNone(unlimited), preserving existing behavior.
Changed
- Tightened the mypy configuration:
no_implicit_optional,warn_unused_ignores, andwarn_redundant_castsare now on. Removed the blanketimplicit_optional = true. Cleaned up the stale# type: ignorecomments this surfaced. - Completed type annotations across the codebase and enabled mypy
disallow_incomplete_defs, so partially-annotated functions now fail type checking. Annotations only — no runtime behavior changed. - Dropped the separate "Generated client syntax checks" CI job.
Fixed
- Synchronous lifecycle handlers (
startup/shutdownevents) and synchronous exception handlers now run in a thread pool instead of directly on the event loop, so a blocking call in one no longer stalls the server. - Content-Type matching is now case-insensitive, per RFC 7231. Requests sent
with e.g.
Application/JSONorMultipart/Form-Dataare negotiated and parsed correctly instead of falling through to the wrong parser. AffectsRequest.is_json,Request.media()format auto-detection, and multipart form parsing (including an uppercasedBoundaryparameter name). ResponderServer.__init__'slimit_max_requestsparameter was annotatedintbut defaulted toNone; it is nowint | None.
v7.1.3 - 2026-07-01
Fixed
RedisBackend/AsyncRedisBackendrate limiters now increment the counter and set its expiry in a single atomic server-side step (LuaEVAL). The previous separateINCR+EXPIREcalls could leave a key without a TTL if the process died between them, locking that client out until manual eviction.- Background tasks submitted via
api.backgroundare now drained on application shutdown instead of being abandoned, via a registered shutdown handler that callsBackgroundQueue.shutdown().
Changed
- CI now runs
ruff checkandmypyon every push and pull request (newLint & Typesworkflow), not just during the manual release guard.
v7.1.2 - 2026-06-30
Added
- New example apps:
examples/todo.py,examples/fortunes.py,examples/shortlinks.py,examples/tarot.py, andexamples/webhooks.py. - Modernized
examples/helloworld.py,lifespan.py,marimo_mount.py,rest_api.py,sse_stream.py,user.py, andwebsocket_chat.py, with matching coverage intests/test_examples.py.
Fixed
RateLimiterandenable_logging'sLoggingMiddlewarekeyed/recorded the client IP from the TCP peer only, which behind any reverse proxy is the proxy itself — every visitor shared one rate-limit bucket and access logs showed the proxy's address for every request. Both now accepttrust_proxy_headers=Trueto read the real client fromX-Forwarded-For/X-Real-IPinstead (opt-in, since trusting those headers without a proxy in front lets a client spoof its own address).
v7.1.1 - 2026-06-30
Added
examples/atelier.pyis now the canonical showcase app and contract fixture for auth policies, OpenAPI metadata, response helpers, problem details, and generated clients.
Fixed
- Auth-injected
user,principal, andauthparameters are no longer treated as request-body Pydantic models on routes with JSON bodies. - Generated clients now keep scanning success responses until they find a JSON schema, instead of giving up when an earlier 2xx response has no body.
v7.1.0 - 2026-06-30
Added
api.policy(name, auth)creates named, reusable auth policy wrappers without changing the wrapped auth scheme's runtime or OpenAPI behavior.- Route decorators now accept
responses=,examples=,response_examples=, andopenapi_extra=for nearby OpenAPI operation authoring that deep-merges with generated schemas and framework error responses. resp.created(...),resp.no_content(), andresp.problem(...)provide focused helpers for common REST responses and manual problem-details payloads.- A canonical contract fixture now validates a representative OpenAPI document and generated Python client against the same in-process app.
scripts/release.pyorchestrates the release guard, tagging, GitHub release creation, package build, and Twine upload with a dry-run default.
v7.0.5 - 2026-06-30
Added
- Generated OpenAPI documents are now validated in tests for both OpenAPI 3.0.x and 3.1.x, including problem-details and legacy-error modes.
- A
scripts/release_check.pyguard verifies release metadata, optional clean tree and tag state, tests, lint, types, docs, package builds, wheel contents, andtwine check. - Runtime contracts documentation now describes framework error responses, auth inheritance, dependency lifecycle, response-model failures, and OpenAPI defaults.
Changed
- Wheels now only package
responderand its declared package data, keeping top-level docs, examples, and tests out of the installed wheel. - CI now runs the release guard's metadata, build, wheel-content, and
twine checkpath on every push and pull request.
Fixed
- The Sphinx docs build now treats Markdown files as sources, making existing Markdown project pages and the changelog symlink resolvable from the toctree.
v7.0.4 - 2026-06-30
Fixed
- HEAD responses now send headers without an ASGI response body while preserving
the
Content-Lengtha GET would have produced for non-streaming responses. - Query-string parameters with blank values (for example
?q=) are preserved instead of being treated as missing. - Mutating
req.path_paramsduring a request can no longer poison the route resolution cache for later requests to the same concrete path.
v7.0.3 - 2026-06-30
Fixed
- OpenAPI now documents legacy JSON error responses when
API(problem_details=False)is set, and omits theProblemDetailsschema in that mode. auth.optional()now rejects malformedAuthorizationheaders instead of treating them as anonymous requests.problem_handlernow receives the underlying exception for validation, response-model, and after-hook failures, and a failing handler falls back to the original framework payload.- Scoped auth insufficient-scope challenges now keep valid
WWW-Authenticateformatting when the base challenge already has parameters. - Dependency-resolution errors no longer duplicate the active dependency in the reported chain.
v7.0.2 - 2026-06-30
Added
API(problem_handler=...)can enrich framework-generatedapplication/problem+jsonpayloads with fields such astype,instance, application error codes, or links.- Problem-details payloads include
request_idwhen request ID middleware or structured logging is enabled. - OpenAPI generation now registers a reusable
ProblemDetailsschema and documents common framework error responses (400,401,403,404,405,413,422,500, and504when configured). - OpenAPI operations now receive default
operationId,summary, and tags when route metadata does not supply them. - Generated Python, JavaScript, TypeScript, Ruby, and PHP clients expose parsed
problem details on
APIError.problem; Python also surfacestitle,detail, anderrorsconvenience attributes. auth.optional()accepts missing credentials while still rejecting invalid credentials, injectingNonefor anonymous requests and documenting optional security in OpenAPI.API(trace_dispatch=True)emits debug logs for the documented dispatch stages: before hooks, auth, dependencies, handlers, and after hooks.responder.testing.assert_problem(...)provides a small assertion helper for problem-details responses.
Changed
- Scoped auth failures now include a
WWW-Authenticateinsufficient-scope challenge when the wrapped scheme provides a challenge. - Dependency-resolution errors include the dependency chain being resolved.
v7.0.1 - 2026-06-29
Fixed
- Problem-details responses for
413errors now use the RFC 9110 titleContent Too Largeconsistently across Python runtimes. - CI now installs the checked-out package editable during tests so cached wheels cannot mask source changes during release validation.
v7.0.0 - 2026-06-29
A major release focused on explicit runtime contracts: problem details by
default, first-class app auth (with scope/role checks), route-level dependency
guards, and clearer optional production-server packaging. Python 3.11 is now the
minimum, and the deprecated request_model= route option has been removed.
Added
API(auth=...)applies auth helpers to routes by default. Routes can opt out withauth=None.ScopedAuthandauth.requires(...)add lightweight scope/role checks on top of existing auth helpers, returning403when the principal lacks a required scope and documenting scoped OpenAPI security requirements.dependencies=[Depends(...)]on route decorators runs dependency-graph dependencies before the handler without injecting an unused return value.responder[server]keeps Granian as an optional production ASGI dependency.uvicornremains in the default install and is still used byapi.run()unlessserver="granian"is requested.api.run(server="granian")runs the current app with Granian's embedded ASGI server whenresponder[server]is installed, and raises a targeted install error when it is missing.- Route dispatch ordering for before hooks, auth, dependency-guards, handler, and after hooks is now part of the documented v7 behavior.
- A v7 migration guide covering default error responses, auth inheritance, route dependency guards, removed request-model compatibility, and the server extra.
Changed
- Framework-generated errors now use
application/problem+jsonby default.errorsremains an extension member when present (for example on 422s), so migration is media-type oriented instead of response-shape oriented. Passproblem_details=Falseto keep the legacy JSON/plain-text negotiation. - Problem-details responses are serialized as JSON bytes regardless of the
request's
Acceptheader, and unhandled 500s use the same problem-details contract by default. - Explicit
Depends(...)parameters and registered dependencies take precedence over auth principal-name injection when they use the same handler parameter name; the authenticated principal remains available on request state. req.methodnow returns an exactstr; the no-op exportedHTTPMethodsubclass has been removed.- Python 3.11 is now the minimum supported Python version; internal datetime
handling uses the stdlib
datetime.UTCalias accordingly.
Removed
- The deprecated
request_model=route option andreq.state.validatedcompatibility path have been removed. Use required Pydantic-typed handler parameters for request-body validation. - The deprecated private
_resolve_dependencyroute helper has been removed; the request dependency graph is resolved through the shared resolver.
Fixed
- Byte-range requests now cap the number of requested ranges and coalesce overlapping or adjacent ranges before building single or multipart responses.
- Request-scoped dependency caching now distinguishes bound methods by instance, avoiding cross-instance value reuse.
- WebSocket routes now resolve inline
Depends(...)parameters and run route-localafter=hooks. - Exceptions raised by after hooks now produce a controlled 500 response using the normal error contract.
- OpenAPI schemas now default missing API metadata to valid string values
instead of emitting
nullfor requiredinfofields.
v6.6.1 - 2026-06-29
A small follow-up release focused on problem-details consistency, packaging metadata, documentation, and internal route-dispatch maintainability.
Changed
problem_details=Truenow also applies to production response-model validation failures, returningapplication/problem+jsonfor those framework-generated500responses.- Refactored HTTP route dispatch internals into smaller helpers without changing public behavior.
- Updated project license metadata to the modern SPDX form used by current packaging tools.
- Refreshed the README with concise examples for route-local hooks, local dependencies, route-level auth, file-upload saving, and problem-details responses.
v6.6.0 - 2026-06-29
A backward-compatible release that adds route-local controls, explicit local dependencies, problem-details errors, upload saving, and multipart byte ranges.
Added
- Route-local hooks with
before=andafter=onapi.route()and verb decorators. Global hooks still run, while route-local hooks apply only to the decorated endpoint. - Route-level auth enforcement with
auth=. Auth helpers such asBearerAuthnow enforce access, register their OpenAPI security scheme when OpenAPI is enabled, and inject the authenticated principal intouser,principal, orauthhandler parameters. Depends(...)for explicit per-route dependency providers without app-wide registration. Providers can be sync, async, generator, or async-generator callables and can receive the current request or registered dependencies.problem_details=TrueonAPI(...)for RFC 9457-style framework errors usingapplication/problem+json.await upload.save(path)on injected/uploadedUploadFileobjects, with streaming writes and optional parent-directory creation.- Multipart byte-range responses for
resp.file()andresp.stream_file()when clients request multiple ranges.
Changed
- Response format encoders now respect an explicit
Content-Typealready set on the response instead of overwriting it during serialization.
v6.5.3 - 2026-06-29
Fixed
- Generated TypeScript clients now type-check under
tsc/deno check. The opt-invalidatehelpers added in 6.5.2 emitted untyped parameters (implicitany) in the shared JS/TS validation code; the TypeScript output is now annotated. JavaScript output is unchanged.
v6.5.2 - 2026-06-29
A backward-compatible follow-up adding opt-in generated-client validation.
Added
- Generated Python, JavaScript, and TypeScript clients now support opt-in
runtime schema validation with
validate=True/validate: true, raisingAPIValidationErrorwhen outgoing JSON bodies or successful JSON responses do not match the generated OpenAPI schemas.
v6.5.1 - 2026-06-29
A backward-compatible follow-up that rounds out the client generator.
Added
responder client <target>CLI subcommand generates a client from the same import target asresponder run—--lang,--class-name, and--output/-oselect the language, class name, and destination (stdout by default).- Generated Python clients emit
TypedDictdefinitions and TypeScript clients exported interfaces for OpenAPI component schemas, used for the request-body and success-response method signatures.
v6.5.0 - 2026-06-29
A backward-compatible release adding first-class client generation.
Added
responder.ext.clientgengenerates small Python, JavaScript, TypeScript, Ruby, and PHP clients from Responder's OpenAPI schema.generate_client(...)returns source code, andwrite_client(...)writes a module to disk.API.generate_client(...)is the app-level convenience wrapper:api.generate_client(class_name="ServiceClient")returns source, whileapi.generate_client("clients/service.py", class_name="ServiceClient")writes a ready-to-import client module. Passlanguage="typescript"(orjavascript,ruby,php,python) to select the target.- Generated clients include method signatures from path/query parameters, JSON
request bodies, bearer/basic/API-key header helpers, structured
APIError, real HTTP transport, typed Python/TypeScript signatures where schema permits, and a Pythonsession=hook so the same generated client can callapi.requestsin tests.
v6.4.0 - 2026-06-29
A backward-compatible release that makes typed path parameters and resumable downloads much sharper in practice.
Added
- Typed path parameters now work even on plain route segments. A route like
/users/{id}can now declareid: int(orUUID, etc.) and Responder validates/coerces the path value into that type, returning a422on bad input instead of passing a raw string through. WebSocket routes get the same coercion and close invalid typed path parameters with1008. - Path-parameter OpenAPI got smarter. The generated schema now reflects:
- a bare handler annotation on a plain
{id}segment, Path(...)aliases/constraints/description metadata, and- the built-in
{id:uuid}convertor as{type: string, format: uuid}. Class-based views now generate method-specific parameters fromon_get/on_post/ etc., so one method's query parameters no longer disappear from the schema.
- a bare handler annotation on a plain
If-Rangesupport for file responses.resp.file(),resp.stream_file(), andresp.download()now honorIf-Rangeso a stale resumable-download request automatically falls back to the full body instead of returning a partial response for the wrong representation.
v6.3.1 - 2026-06-29
Fixed
- OpenAPI now emits query/header/cookie/path parameters at the operation
level rather than the path-item level. The path-item placement (present since
the typed-OpenAPI feature in 5.0) leaked a route's parameters onto sibling
methods sharing the same path — e.g. a
@api.postinherited a@api.get's query parameters. Each operation now carries only its own parameters (also the placement most tooling and codegen expect).
v6.3.0 - 2026-06-29
A backward-compatible release adding sorting & filtering helpers that complete the list-endpoint story alongside pagination.
Added
-
responder.ext.query— in-memory helpers for list endpoints (dicts or objects, no ORM coupling):sort_items(items, spec, *, allowed=None)sorts by aname,-createdspec (-= descending, multiple keys,Nonesorts last). Passallowed=for a client-supplied sort so users can't order by arbitrary attributes — an out-of-list (or incomparable) field returns400.parse_sortis exposed for sorting in the database yourself.filter_items(items, filters)appliesfield == valueequality and skipsNonevalues, so optionalQuerymarkers pass straight through.
Together with
Querymarkers andresponder.ext.paginationthey make a complete filter → sort → paginate pipeline.
v6.2.0 - 2026-06-29
A backward-compatible release adding pagination helpers for list endpoints.
Added
responder.ext.pagination— a genericPage[T]response envelope (items,total,page,size,pages) and apaginate(items, *, page, size, total=None)helper.paginateslices an in-memory collection by default, or wraps an already-sliced page when you passtotal=(e.g. aLIMIT/OFFSETquery). Pairs with the typedQuerymarkers andresponse_model=Page[Item]— the OpenAPI schema documents the envelope inline, referencing your element model.- Parametrized generic models (e.g.
Page[Item]) are now emitted inline in OpenAPI (referencing their element components) rather than registered under a bracketed — and spec-invalid — component name.
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!