122 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
v9.2.0 - 2026-07-14
Responder 9.2 makes typed SSE and NDJSON complete incremental response contracts, from per-item runtime validation through OpenAPI and generated streaming clients.
Added
- Typed event streams:
@api.sseand@api.ndjsoninfer item contracts from sync or async iterator return annotations (or explicitevent_model=/item_model=), validate and serialize every yielded item with Pydantic, and preserve backpressure and cancellation cleanup.responder.SSE[T]adds typeddataplus standardevent,id,retry, and comment metadata; SSE routes retain heartbeats andLast-Event-IDsupport. - OpenAPI now documents typed streams with their wire media type and item schema. Generated Python, JavaScript, TypeScript, Ruby, and PHP clients consume them lazily as iterators, including typed SSE metadata events.
Changed
- An immediately available first stream item is validated before response
headers, allowing contract failures to return
500. Failures after a stream begins are logged and terminate it without emitting the invalid item.HEADremains body-free and does not start the producer.
v9.1.0 - 2026-07-12
Responder 9.1 makes Python return annotations complete HTTP response contracts, from runtime validation through OpenAPI and generated clients.
Added
- Return annotations are now complete response contracts. Any supported
Pydantic
TypeAdaptershape — models, dataclasses, typed dictionaries, collections such aslist[Item], unions, generic pagination models, and JSON scalars — drives runtime validation and serialization, OpenAPI response schemas, and generated-client types. Class-based-view method annotations and Flask-style tuple annotations infer the same way. Inferredstrandbytesresponses document and emittext/plainandapplication/octet-stream; other inferred contracts useapplication/json. - Handlers may return
bool,int, andfloatJSON scalars directly, in addition to the existing dictionaries, lists, strings, bytes, models, and dataclasses. responses={404: ErrorModel}now declares a status-specific response contract used for runtime validation and OpenAPI, including alternate success statuses. A response mapping may providemodelalongside a custom description, headers, examples, or other OpenAPI metadata.
Changed
- Response contracts now validate the final success body after after-request
hooks. A body whose shape or response channel contradicts its declared model
fails closed with
500, including a list returned for a single-object model;response_model=Falsereliably disables inference and validation. - Flask-style return tuples now require exactly
(body, status)or(body, status, headers), a status integer from 100 through 599, and a header mapping. Malformed tuples fail clearly instead of being partially applied or silently ignored. - Routes with unresolved or unsupported return annotations now log a focused
registration warning with the
response_model=Falseopt-out instead of silently dropping the inferred contract.
Fixed
- Informational,
204,205, and304responses no longer send a body even if a handler returned or assigned one. Their inferred OpenAPI success responses omit content as well;HEADcontinues to validate the correspondingGETresponse while sending headers only.
v9.0.1 - 2026-07-06
A bug-fix release from a post-9.0.0 adversarial scan: several v9-new surfaces (CSRF, proxy headers, streaming form parsing, rate limiting, GraphQL, OpenAPI caching) had crash or enforcement defects. Security- relevant; upgrading from 9.0.0 is recommended.
Fixed
- CSRF: a non-ASCII submitted token (a fully client-controlled
csrf_tokenform field orX-CSRF-Tokenheader) crashedhmac.compare_digestand returned500instead of403; tokens are now compared as UTF-8 bytes. - CSRF: the per-route
csrf=override was stored on the shared view object, so re-registering a function withcsrf=Falsestripped protection from its earlier routes, and a CBV registered withcsrf=Falseleaked the exemption to subclasses through the MRO. Each registration's value is now frozen onto its ownRoute. - CSRF:
csrf=Trueon a WebSocket route was accepted but never enforced; it now raises at registration. - Proxy headers: a forwarded
Host(orContent-Length) whose port/value is a non-ASCII digit character (e.g.U+00B2, whichstr.isdigit()accepts butint()rejects) raised an unhandledValueError. InProxyHeadersMiddleware— the outermost layer — this sent no response at all; both call sites now require.isascii()beforeint(). - Rate limiting:
@limiter.limiton a class-based-view method returned500on every request because the wrapper mislocated the request/response past the bound method'sself; they are now located by type. - GraphQL: a non-object JSON body (e.g. a JSON array) and a multipart POST
without a
queryfield both returned500; both now return400. - Forms: a malformed or oversized multipart body on the buffered parse branch
raised an unhandled
500instead of the documented400. - OpenAPI: the generated-document cache ignored
request_timeoutandproblem_details, so changing either after the first render served a stale schema; both are now part of the cache key.
v9.0.0 - 2026-07-05
A major release: streaming multipart uploads, opt-in CSRF protection, reverse-proxy header support, and the v8.1 deprecation path becoming the default behavior. See the v9 migration guide for the full upgrade path.
Added
- Opt-in CSRF protection:
API(csrf=True)requires unsafe requests (POST/PUT/PATCH/DELETE) to present the session-bound token fromreq.csrf_tokenin anX-CSRF-Tokenheader orcsrf_tokenform field ({{ req.csrf_input }}renders the hidden input for templates), rejecting the rest with a403. Per-route overrides via@api.route(..., csrf=False)(webhook receivers) orcsrf=True(protect a single route). The token check shares the request's single streaming form parse. Off by default; requires sessions. - Proxy-headers support:
API(trust_proxy_headers=True)now installs a middleware honoring RFC 7239ForwardedandX-Forwarded-Proto/X-Forwarded-Host/X-Forwarded-For/X-Real-IP, rewriting the request's scheme, host, and client address to what the original client sent — HTTPS detection, redirects, URL building, trusted-host validation, and logged client IPs are then correct behind nginx/Caddy/load balancers. - Streaming multipart uploads: multipart bodies now parse incrementally off
the wire, spooling file parts to temporary files (rolling to disk past
~1 MB) instead of buffering the whole body in RAM.
File()markers,req.media("files"), andreq.media("form")all share the single streaming parse, andmax_request_sizeis enforced chunk-by-chunk as the body arrives — uploads are bounded by disk, not memory.
Changed
trust_proxy_headers=Truepreviously only affected the client IP recorded byenable_logging; it now rewrites the connection's scheme, host, and client address for the whole stack (see Added). Apps that enabled it purely for logging get the same logged IPs, plus correct scheme/host handling.max_request_sizenow defaults to 100 MiB instead of unlimited; passAPI(max_request_size=None)for the previous behavior. Oversized bodies get a413as before.- Generated OpenAPI operations now document Responder's operational responses:
CSRF-protected unsafe routes get
403, rate-limited routes get429(and fail-closed backend503), and request-size413is emitted only when a body cap is active. Rate limiter429/503responses now honorAPI(problem_details=True)and keep the legacy{"error": ...}JSON shape whenproblem_details=False. - Because multipart parsing streams, the raw body is consumed by the parse:
await req.contentaftermedia("form"/"files")raises a clearRuntimeError(awaitreq.contentfirst to keep the buffered, replayable 8.x behavior), and handlers usingFile()/Form()markers no longer have the raw multipart body available (matching Starlette/FastAPI). Multipart text fields read viamedia("form")now share the streaming parser's limits (1000 parts, 1 MB per text field,400beyond them) and lossily decode invalid UTF-8 instead of silently dropping the field. - Prepared the v9 behavior defaults: explicit
port=now wins over a conflictingPORTenvironment variable, bareadd_route()calls require an explicit endpoint by default,Decimalvalues serialize as strings by default, and GraphQL partial-data responses default to HTTP200. The explicit compatibility switches for these legacy behaviors remain available without deprecation warnings while apps migrate.
Removed
- Removed the deprecated
API.session()accessor. Useapi.requestsorapi.test_client(...)instead.
v8.3.0 - 2026-07-05
Added
- Added
api.test_client(...), a configurable form ofapi.requestsfor custombase_urlvalues and StarletteTestClientoptions such asraise_server_exceptions=False, without relying on deprecatedapi.session(). - Added v9 compatibility opt-ins:
API(json_decimal="string")serializesDecimalvalues as precision-preserving strings, andapi.graphql(..., partial_data_status=200)returns GraphQL partial-data responses with HTTP200per the GraphQL-over-HTTP spec. Apps can also passAPI(implicit_static_fallback=False)to disable bareadd_route()static fallbacks early, andport_precedence="explicit"toserve()/run()so an explicitport=wins over a conflictingPORTenvironment variable. - Added
head()andoptions()shortcut decorators onAPI,RouteGroup, and standaloneRouterinstances.
Changed
- Declared the Starlette test-client dependencies explicitly (
httpxandhttpx2), raised the Uvicorn and Granian dependency floors to current compatible releases, and allowed Sphinx 9 for documentation builds. - Removed the PyPy classifier and remaining PyPy-specific test-extra guidance now that future CI runs target CPython only.
v8.2.3 - 2026-07-05
Fixed
RedisSessionBackendandAsyncRedisSessionBackendnow write sessions with RedisSETplus expiry (EX) when the client supports it, avoiding thesetexdeprecation warning emitted by current redis-py/fakeredis while preserving a fallback for older or custom clients that only exposesetex.
Documentation
- Added a runnable examples guide, surfaced it in the sidebar navigation, and linked the existing v6 migration guide from the project navigation.
- Updated the docs Makefile so
make htmluses the declared documentation dependencies throughuv run --extra docs sphinx-build.
v8.2.2 - 2026-07-03
Fixed
RateLimiter.limitnow propagates the decorated handler's return value, so return-value-style handlers (return {...},return text, orreturn data, status[, headers]) compose with the decorator. Previously the wrapper discarded the return value in both the sync and async branches, so any@limiter.limithandler that returned data instead of mutatingrespproduced an empty200response. Mutation-style handlers were unaffected, which is why it went unnoticed. The over-limit path is unchanged: a request past the limit still gets a429withRetry-Afterand the handler is not invoked.
v8.2.1 - 2026-07-03
A security patch closing two response-injection gaps found by an adversarial sweep over the non-auth framework surface (the auth extension was hardened in 8.2.0). Both fixes are defense-in-depth and change no legitimate behavior.
Security
- Response header values are now stripped of CR, LF, and NUL before they reach
the ASGI layer, closing an HTTP response-splitting / header-injection vector.
A user-controlled value written to a header — most commonly a redirect target
via
Response.redirect(location)orResponse.created(location=...), but also anyresp.headers[...] = ...assignment — could previously carry a bare\r\nonto the wire, letting an attacker inject additional response headers (e.g.Set-Cookie) or split the response on ASGI servers/proxies that don't reject embedded control bytes. The framework already scrubbed CR/LF fromContent-Dispositionand Server-Sent Events fields; this extends the same discipline to every response header. - The access-log middleware (
responder.ext.logging) now strips control characters (C0 range and DEL) from the request method and path before logging them. Because ASGI servers percent-decodescope["path"], a request to a path like/x%0d%0a[INFO]...previously wrote a raw CR/LF into the log line, letting an attacker forge log records in a plaintext log sink. Spaces and non-ASCII in a decoded path are preserved — only control bytes are removed — mirroring the validation already applied to inboundX-Request-ID.
v8.2.0 - 2026-07-03
A security-focused release hardening the JWT/OAuth2 authentication introduced
in 8.1.0. A dedicated adversarial pass over the new auth code surfaced nine
issues (all in responder.ext.auth); every fix ships with a regression test.
Security
JWTAuthrejects a plaintext (http)jwks_urlat construction unless it targets loopback (localhost/127.0.0.1/[::1]) orallow_insecure_jwks=Trueis passed. A man-in-the-middle on a plaintext JWKS fetch could serve attacker-controlled signing keys — a full authentication bypass.JWTAuthno longer lets an attacker-controlled, unknownkidamplify into unbounded blocking JWKS refetches. The JWKS client now caches keys, and a missingkidtriggers at most one network refresh per short cooldown window (otherwise rejecting with401and no fetch), closing an unauthenticated denial-of-service vector. The JWKS cache is lock-guarded for the threadpool.JWTAuthrejects tokens without anexpclaim by default (newrequire_exp=True); previously an expiry-less token was accepted indefinitely. See the note under Changed to restore the old behavior.OAuth2Authno longer mutates token-introspection principals in place, and a previously-stampedscopesvalue no longer short-circuits a fresh introspection result — fixing privilege retention where a token that the authorization server had downscoped kept its old, broader scopes.
Changed
JWTAuth(require_exp=...)defaults toTrue, so tokens without anexpclaim are rejected. This is stricter than 8.1.0, which accepted them. If you intentionally issue non-expiring JWTs, passrequire_exp=False.
Fixed
JWTAuth: a JWKS-configured scheme no longer raises an uncaughtTypeError(HTTP 500) when sent anHS256token whosekidresolves to a public key — the mismatch now rejects with401.JWTAuth:audience=Nonenow actually disablesaudverification, so tokens carrying anaudclaim (virtually every OIDC access token) are accepted instead of being rejected with401. Whenaudienceis set, it is still enforced.JWTAuth/OAuth2Auth: thescopeandscpclaims are now unioned rather than the first-present one winning, so an Azure AD / Auth0 token that carries its granted scopes inscp(with an emptyscope) is no longer denied403.- OAuth2 flows: constructing a flow with an empty required URL
(
authorizationUrl/tokenUrl) now raisesValueErrorinstead of silently emitting an OpenAPI security scheme that fails validation.
Packaging
- The
orjsonandfakeredis[lua]test dependencies (compiled, without wheels on free-threaded CPython or PyPy) moved out of thetestextra into a version-scoped CI step, sopip install responder[test]on free-threaded 3.14 no longer fails trying to build them from source.
v8.1.0 - 2026-07-03
Added
JWTAuthinresponder.ext.auth:Authorization: Bearerauthentication with full JWT validation — signature (HS256 by default; RS256/ES256 and friends whencryptographyis installed),exp/nbf/iatwith a configurableleeway, andaudience/issuerchecks. Keys come from a staticsecret(HMAC secret or PEM public key) or ajwks_url, resolved by the token'skidheader with cached key sets that refresh automatically on rotation (the JWKS fetch runs off the event loop). The validated claims become the injected principal — or passverify=to map them onto your own user object — and the token'sscope/scpclaim is normalized into the scopes that feedrequires(). Invalid tokens reject with401+ aBearerchallenge, missing scopes with403+insufficient_scope. Requires the newresponder[jwt]extra (PyJWT), imported lazily with a helpful error.OAuth2Authand flow descriptions (OAuth2AuthorizationCodeFlow,OAuth2ClientCredentialsFlow,OAuth2PasswordFlow) inresponder.ext.auth: the OpenAPI document emits a propertype: oauth2security scheme with flows and scopes, lighting up Swagger UI's Authorize button. At runtime it acts as a resource server — bearer tokens are validated locally through ajwt=JWTAuthor averify=token-introspection callable. Convenience constructorsOAuth2Auth.authorization_code(...),.client_credentials(...), and.password(...)cover the single-flow cases; both compose withrequires()/optional()/AuthPolicyand group-level auth viaRouter/include_router.responder.Problem— a raisable RFC 9457 problem-details exception. RaiseProblem(409, "detail", title=..., type=..., instance=..., **extensions)from any view, hook, or dependency to short-circuit with a fullapplication/problem+jsonresponse, rendered through the same pipeline as framework errors (problem_handlerenrichment, request IDs, the app's JSON encoder).instancedefaults to the request path, and because it subclasses Starlette'sHTTPException, existing handlers and middleware treat it likeabort().abort()now accepts the RFC 9457 members —title=,type=,instance=,errors=, and arbitrary extension keywords — raising aProblemthat carries them into the payload. Plainabort(status, detail=..., headers=...)still raises a regularHTTPException, unchanged.@api.middleware("http")— a decorator for function-style HTTP middleware:async def mw(request, call_next) -> response, no ASGI class boilerplate. Synchronous functions run on a dedicated thread pool and receive a blockingcall_next. Function middleware registers through the same stack asadd_middleware, so the two compose: the most-recently-registered user middleware is outermost. Backed by the new publicresponder.middleware.FunctionMiddlewareadapter; WebSocket and lifespan traffic pass through untouched.req.headers.get_list("X-Forwarded-For")returns every raw line of a repeated request header, in the order received — duplicate header lines are no longer silently collapsed. Single-value access is unchanged: last wins.- Write-side HTTP preconditions (RFC 9110 §13): on state-changing methods,
when
resp.etagorresp.last_modifiedis set,If-Match(strong comparison,*supported),If-Unmodified-Since(only whenIf-Matchis absent), andIf-None-Matchare evaluated, and a failing precondition answers412 Precondition Failed. Repeated header lines are folded per the RFC. Like the existing304handling, preconditions are checked only when the matching validator is set. req.preferred_media_type(candidates)— ranks candidate media types (or bare subtype tokens) by the q-value of the most specific matchingAcceptrange and returns the client's preferred one, orNonewhen it accepts none. Ties, and requests without anAcceptheader, keep the candidate order.- Pydantic body-model injection for class-based views:
on_post(self, req, resp, *, item: Item)receives the parsed, validated body exactly like function views (automatic422on invalid input). The body is parsed once per request, soon_requestand a method handler declaring the same model share one validated instance. Query(),Header(),Cookie(), andPath()markers now resolve on WebSocket handlers from the handshake request (query string, headers, cookies), with the same Pydantic coercion as HTTP views. A missing required value or failed coercion closes the socket with1008(policy violation) before the handler runs.- Bind an entire form to a Pydantic model: a parameter annotated with a
BaseModelsubclass and aForm(...)default validates the parsed urlencoded/multipart form into the model in one shot — repeated keys collect into list fields, uploads bind toUploadFilefields, aliases are honored, and failures return the same422payload as JSON body models with["form", <field>]error locations. The OpenAPI schema documents the model as themultipart/form-data/application/x-www-form-urlencodedrequestBody. RateLimiteracceptskey=— a callablereq -> strnaming the bucket a request counts against (an API key, a user id) instead of the client IP (still the default and the fallback) — andfail_open=: when the backend errors out (e.g. Redis is unreachable),fail_open=Truelogs a warning and lets the request through, while the defaultFalseanswers503instead of an unhandled 500. Every response now carriesX-RateLimit-Reset; the backendhit()/ahit()contract grew a thirdreset_afterelement, with 2-tuple third-party backends still accepted (the header is omitted).MetricsCollectoracceptsbuckets=— ascending histogram bucket bounds — exposed asAPI(metrics_buckets=...), and/metricsnow exports aresponder_requests_in_flightgauge.responder.ext.pagination.set_pagination_headers(req, resp, page)emits the RFC 8288Linkheader (rel="first"/"prev"/"next"/"last", built from the request URL with all other query parameters preserved) plusX-Total-Count, alongside the existingPagebody envelope.- CLI:
responder runaccepts--host=<addr>,--port=<n>, and--server=<name>(uvicornorgranian) to control binding and backend, plus--reloadfor uvicorn auto-reload (requires amodule:attrtarget). - Optional orjson JSON backend: when orjson is installed
(
pip install "responder[orjson]"), JSON responses encode via orjson — typically several times faster, with less event-loop blocking. CustomAPI(encoder=...)hooks keep working, andjson_ensure_ascii=Truestays on the stdlib (orjson is UTF-8-only). Decoding continues to use the stdlib so arbitrary-precision integers stay exact. Encoded output differs only in whitespace, except floatnan/infserialize asnullinstead of the non-standardNaN/Infinity. responder.testing.AsyncTestClient: the async mirror ofapi.requests— anhttpx.AsyncClientdispatching in-process;async withruns the app's lifespan. Backed byASGIStreamingTransport, which streams response bodies live, so endless SSE streams can be read and closed mid-flight.- SSE test helpers in
responder.testing:parse_sse(text),iter_sse(response), andcollect_sse(response)parsetext/event-streambodies intoSSEEventobjects (data/event/id/retry, plus.json()), skipping heartbeat comments — the client-side mirror ofresp.sse. - Test coverage for the Redis session and rate-limit backends against
fakeredis(sync and async, including the atomic INCR+EXPIRE Lua script), covering TTL/expiry, key prefixes, middleware round-trips, and outage behavior. status_code=route option:@api.route("/items", methods=["POST"], status_code=201)pre-seedsresp.status_code(an explicit assignment in the view still wins) and documents the success response under that status in the OpenAPI operation instead of a hardcoded200. Works with the method sugar,api.group(), and standaloneresponder.Routerdeclarations.
Changed
- Content negotiation honors the client's q-ranked
Acceptpreference order when a response could serialize to multiple registered formats, instead of always serving the first registered format:Accept: application/yaml;q=0.9, application/json;q=0.1now gets YAML where it previously got JSON. Behavior changes only when a client explicitly ranks formats — spec-correct per RFC 9110 §12.5.1; without anAcceptheader (or when everything ties) JSON remains the default. - Class-based views now auto-answer
OPTIONS. A CBV that defines neitheron_optionsnoron_requestresponds toOPTIONSwith200and anAllowheader listing the methods it serves, instead of405— the same treatment method-restricted function routes already got (RFC 9110 §9.3.7). TheAllowheader on a CBV's405response now includesOPTIONStoo, keeping the advertised methods consistent between the two paths. - Class-based views now reject unsupported HTTP methods with
405before validating typed parameters, so a request using a method the CBV does not serve gets405(withAllow) rather than a422from a handler that would never run — matching how function routes have always ordered method dispatch and parameter validation. Supported methods still validate and return422on bad input. RequestIDMiddlewareandLoggingMiddlewarenow share one request-ID policy: an inboundX-Request-IDis honored when it is at most 128 printable ASCII characters (\x21–\x7e, so namespaced ids likegateway:abc123and base64-flavored ids pass, while control characters and oversized values are replaced to prevent header/log injection and bloat), and both mint full UUID4s otherwise —LoggingMiddlewarepreviously minted 8-hex-char ids (32 bits of entropy), so a busy fleet risked collisions and flippingenable_loggingchanged the ID format.setup_loggingis now exported in__all__.- Coverage is now enforced (
fail_under = 80, previously 0) and measures only theresponderpackage, so generated clientgen artifacts andexamples/*.pyno longer pollute the report. - The generated OpenAPI document is cached instead of rebuilt on every request to the schema route or docs page; registering a route, schema, or security scheme invalidates the cache.
Fixed
- Session values that work with
MemorySessionBackend—datetime,date,time,Decimal,UUID,set,frozenset,bytes— no longer crash the Redis session backends with a hard-to-debug 500: the new defaultJSONSessionSerializerround-trips those types (and refuses user data that collides with its reserved type tag), and aserializer=parameter accepts any customdumps/loadscodec. The storable-values contract is documented for all backends. - Prometheus
/metricsoutput escapes backslashes, double quotes, and newlines in label values per the exposition format, so a route pattern containing them can no longer break the whole scrape. TheRateLimiterdocstring no longer claims "token bucket" (the memory backend is a sliding window; the Redis backends are fixed-window). - Registering two different security schemes under the same name now raises instead of silently overwriting (idempotent re-registration of the same scheme, which routing performs per route, is still fine).
- CLI: bare
responderwith no subcommand prints the usage summary and exits non-zero instead of silently exiting0.
Deprecated
API.session()— use theapi.requestsproperty. Emits aDeprecationWarning; removal in 9.0.- The
PORTenvironment variable overriding an explicitly passedport=inserve()/run(). Warns only when both are set and differ; the environment variable still wins until 9.0, when explicitport=will take precedence. - Calling
add_route()without an endpoint (implicitly registering a static-fallback route). Pass an endpoint explicitly (withdefault=Truefor a catch-all); the implicit behavior is removed in 9.0. - Lossy
decimal.Decimal-to-float JSON serialization. Warns once per process when a bareDecimalreaches the built-in encoder; 9.0 will serializeDecimalas strings. Convert explicitly or pass a customencoder=. - GraphQL responses returning HTTP 400 when the result contains partial data alongside errors. Warns once per process; 9.0 will return 200 for such responses per the GraphQL-over-HTTP spec (no-data error responses keep 400).
v8.0.2 - 2026-07-03
Fixed
ResponderServer's private process-termination method no longer shadowsthreading.Thread._stop(). On Python ≤ 3.12 and PyPy,Thread.join()callsself._stop()internally, so joining aResponderServerinvoked the process-termination logic (marking the server as stopping and skipping the Thread's own bookkeeping) instead of just waiting. Python 3.13+ was unaffected. Latent since the class was introduced; surfaced by the 8.0.1 signal-handler restore tests.
v8.0.1 - 2026-07-02
Added
- A "Migrating to v8" guide in the documentation, covering every 8.0 breaking
change with before/after examples — implicit
static/removal,RouteNotFoundErrorand percent-encoding inurl_for, the 307 redirect default, text-response charsets,application/yaml, empty-YAML-body 400s, and the new dependency declarations and floors — plus a pointer to the standalone-routers guide.
Fixed
- Trailing-slash redirects no longer crash on non-latin-1 paths: the
Locationheader is percent-encoded (GET /日本/now answers307instead of a 500 from aUnicodeEncodeError), and the redirect now matches the alternate path regardless of request method —POST /page/redirects to/pagejust like GET does, instead of falling through to a 404. - A sync view that outlives
request_timeoutcan no longer corrupt the504 Gateway Timeoutresponse: the timeout reply is built on a freshResponse, so the abandoned thread's late writes land on an inert object. Headers and cookies set by completedbefore_requesthooks (request IDs, CORS) are carried onto the 504; stale body-framing headers are not. - The route-resolution cache evicts its least-recently-used entry when full instead of clearing all 1024 entries wholesale, so high-cardinality parameterized paths no longer periodically wipe the hot static entries.
- Route registration errors are real exceptions instead of
assertstatements (which vanish underpython -O): a route path without a leading/, an unknown path convertor, and an unknown event type all raiseValueErrornaming the offender, and a duplicate path parameter (/x/{id}/{id}) is caught up front with a clear message instead of a barere.error. - The legacy
on_event("shutdown")lifespan path now mirrors thelifespan=path when a shutdown handler raises: app-scoped dependency teardowns still run andlifespan.shutdown.failedis sent, instead of both being skipped. - Class-based-view 405 responses now include the
Allowheader required by RFC 9110 §15.5.6, listing the methods the class actually serves viaon_*handlers (plus implicitHEADwhenon_getexists). QueryDictitem assignment andupdate()no longer corrupt single-value reads: assigning a scalar (qd["x"] = "hello") previously stored a bare string, so reading it back returned the last character. Scalars are stored as one-element lists, andupdate()from anotherQueryDictmerges the full multi-value lists, matching 8.0.0 semantics.req.accepts()honors media-range specificity per RFC 9110 §12.5.1: an explicitq=0range now excludes the type even when a broader range (*/*ortype/*) would match — and aq=0wildcard of a different top-level type no longer vetoes unrelated bare-token lookups.req.apparent_encodingrecognizes valid UTF-8 bodies with a cheap strict decode and runs chardet detection in a worker thread, so a large body without acharset=parameter no longer stalls the event loop.resp.stream_file()andresp.download()now sendContent-Length(computed exactly for full-body, single-range, and multipart byte-range responses). Replacing the body afterwards (resp.text,resp.problem(),resp.created()) clears the staleContent-Length/Content-Range/Accept-Rangesso the response cannot be misframed.HEADrequests againstresp.file()no longer read the entire file in the threadpool just to discard the body — the headers (including the stat-derivedContent-Length) are sent without touching the file contents.resp.problem()serializes extension members through the same type-aware JSON encoder asresp.media(datetimes, UUIDs, Decimals, sets, dataclasses, Pydantic models, and any customAPI(encoder=...)) instead of barejson.dumps, soresp.problem(409, occurred_at=datetime.now())no longer turns into a 500.resp.stream()andresp.sse()raise a descriptiveTypeErrorwhen given a non-async-generator function, instead of a message-lessassertthat vanished underpython -O.- OpenAPI parameter schemas for enum and nested Pydantic types no longer emit
dangling
$refs or inline$defs: referenced definitions are hoisted intocomponents/schemas(same-named models from different modules get distinct component names) and the generated document passesopenapi-spec-validator. - Generated clients (Python, JavaScript, TypeScript, Ruby, PHP) now actually send the header and cookie parameters they accept.
- Generated clients send
Form()/File()request bodies asapplication/x-www-form-urlencodedormultipart/form-dataper the documented requestBody media type, instead of JSON the server rejected with 422 — in every supported client language. Ruby multipart parts carry proper filenames (with a[filename, content]pair form), and the PHP client distinguishes file specs from plain list values. - WebSocket routes are no longer documented as HTTP GET operations in the OpenAPI schema.
API(docs_route=...)withoutopenapi=now enables schema generation (defaulting to OpenAPI 3.1.0) as documented, instead of serving a docs page pointing at a 404 schema. An app that already registered its own route at the schema path keeps it — the implied route is skipped with a warning.- Required JSON and form request bodies are marked
required: truein the OpenAPI schema, so generated clients require the body instead of defaulting it toNoneand always failing at runtime. - The client generators emit correct code on parameter-name collisions
(
user-idvsuser_id, or parameters namedbody/path/quote): deduplication and reserved-identifier handling are applied consistently to signatures, query/header/cookie maps, and path interpolation in all four generators. - Optional sequence query parameters (
tags: list[int] | None = Query(None)) now bind repeated values as a list instead of returning 422 — marker sequence detection unwrapsOptional/Unionannotations. - An
async defproblem_handleris now applied on every error path — the negotiated 404/500 path, route-level 422/504/405 responses, andresp.problem()— instead of being silently dropped with a logged warning and a never-awaited coroutine. SecurityHeadersMiddlewareno longer crashes every response when a header value isNone— aNonevalue inheaders=now removes that default header (e.g.headers={"x-frame-options": None}for embeddable apps).responder run app.pyregisters the loaded module insys.modulesbefore executing it, so dataclasses, pickling, andtyping.get_type_hintsinside loaded apps work. A pre-existing module with the same name is never clobbered (the target gets a unique key), and only the added entry is removed if execution fails.- Multipart part bodies accumulate in a
bytearrayduring parsing, avoiding quadratic copying when the parser delivers a part in many chunks. ResponderServerno longer crashes when constructed off the main thread or whenis_running()/stop()are called beforestart(). Signal handlers are installed bystart()(main thread only) and restored onstop(), correctly even when multiple servers are stopped out of order.API.path_matches_routeaccepts the plain path string its docstring documents — for HTTP and WebSocket routes — instead of raisingTypeError; ASGI scope mappings are still accepted.API.session(base_url=...)rebuilds the cached test client when called with a differentbase_urlinstead of silently returning the previous client bound to the old address.
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!