Changelog¶
All notable changes to this project are documented here. The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]¶
Added¶
veloceframework[standard]installs uvicorn,brotliandmsgspectogether. (#298)- The documentation site publishes
/llms.txtand/llms-full.txt. (#298) - Python 3.14 is tested in CI and listed in the PyPI classifiers. (#299)
Changed¶
- The
/surpass/documentation pages moved to/why-veloce/; the old paths redirect. (#300)
[0.20.0] - 2026-09-02¶
Security¶
- A route whose unresolved annotation carries any parameter marker is refused, not served unguarded. (#296)
- A websocket listener whose message annotation cannot be resolved is refused, not left unvalidated. (#296)
@app.mcp_tool,@app.mcp_prompt,MCPAuthandMCPAuthorizationServerreject a bare string scope. (#296)Signer.add_fallback_secretrefuses an empty secret, which installed a publicly derivable verification key. (#296)Forwardedwith an unbalanced quote is not trusted; it collapsed the hop count to the sender's choice. (#296)- Repeated
Forwarded/X-Forwarded-*lines are joined in received order; only the first was read. (#296) - A request path with an empty segment no longer matches;
//admin/xreached the handler for/admin/x. (#296) - A CSRF cookie that fails verification is replaced on the refusal; it previously refused every write for good. (#296)
- An unterminated header line is refused once it crosses the header budget; it previously buffered without limit. (#296)
- The automatic WebSocket
PONGrespects write backpressure; aPINGflood queued one reply per ping. (#296) MAX_CONTENT_LENGTHapplies to the first ASGI body message; a chunked body escaped the cap. (#296)Forwardedis trusted only on request; a client-supplied header overrode the proxy'sX-Forwarded-*. (#296)- A slash redirect never emits a protocol-relative
Location, which left the origin for an attacker's host. (#296) url_forpercent-encodes path values; a?,#or/in one injected into the built URL. (#296)- A short-circuited response gets a real CSP nonce; it shipped the fixed token
nonce-None. (#296) completion/completechecks the owning prompt or resource's scopes before running its completer. (#296)- A route-backed tool converts its typed path parameters; the MCP door copied the raw JSON value. (#296)
- A non-latin-1 header value encodes to one token; U+2028 folded it onto a second line. (#296)
Changed¶
- A route whose unresolved annotation carries a parameter marker now raises at registration; import the name at runtime. (#296)
- A websocket listener whose message annotation cannot be resolved now raises at registration; import the name at runtime. (#296)
Security(scopes=...)rejects a bare string; pass["scope"]for a single scope. (#296)//a/band/a//bnow return404; send the canonical path. (#296)ProxyFix(trust_forwarded=...)defaults toFalse; passTruewhere every trusted proxy setsForwarded. (#296)url_foroutput is percent-encoded; a value containing a space or?now appears escaped. (#296)
Fixed¶
- A parameter marker is found when the annotation's subscript base does not resolve. (#296)
X | Nonecarrying an unresolved name behaves asOptional[X]does. (#296)- An annotation whose only unresolved name is a
typingone no longer refuses to register. (#296) - A path parameter with an unresolved annotation registers and reads from the path. (#296)
- A
functools.partialhandler's pre-bound parameter no longer refuses to register. (#296)
[0.19.0] - 2026-09-02¶
Added¶
@app.websocket_listenervalidates each frame against the callback's message annotation. (#295)- A websocket frame that does not match the declared message type closes with
1007. (#295) - An undiscriminated websocket message union is refused at registration. (#295)
- A listener's return annotation documents what the channel sends. (#295)
- A websocket listener validates a dataclass or
TypedDictmessage, as the HTTP body path does. (#295)
Changed¶
Response(background=...)accepts a bare callable and rejects an unsupported value. (#295)- The
fastextra requiresmsgspec>=0.16; earlier versions lackmsgspec.convert. (#295)
Fixed¶
MAX_CONCURRENT_CONNECTIONS = Noneruns the built-in server without a cap, as documented; it raisedTypeErrorand refused every connection. (#294)- A dataclass or
TypedDictresponse model is documented with its own component schema. (#295) - A streaming response from an async generator encodes
strchunks, as the sync path already did. (#295) Request.sessionis typed asSession, exposingpermanentandmodifiedto type checkers. (#295)set_cookieanddump_cookiedeclare theexpirestypes they already accept. (#295)- A handler may return
(body, headers)with any mapping;Headersis not adict, so the framework's own header type was read as a status and answered500. (#292)
[0.18.0] - 2026-08-31¶
Security¶
- The cold ASGI emit path applies the response-splitting guard to
content_type; a CR or LF in it reached the wire as a raw header value. (#289) - An unresolvable annotation no longer erases the whole signature's PEP 593 metadata; a route whose unrelated parameter had a bad annotation stopped enforcing its
Depends()security scheme and served unauthenticated. (#289) Responsecopies theheadersmapping it is given; a handler reusing one dict across requests shipped a previous request'sSet-Cookie, leaking another user's session. (#289)ProxyFixcountsForwarded:hops correctly when an element carries a backslash; one outside a quoted string hid the following comma, merging the trusted proxy's element into the client's and putting attacker text intorequest.remote_addr. (#289)- The built-in server serves a request whose target is split across two reads; it answered
400, and measuredMAX_URL_SIZEagainst the last fragment rather than the whole target. (#289) request.authorizationandHTTPBasicdecode aBasicpayload through one implementation, so a header with extra whitespace no longer yields credentials from one and a401from the other. (#289)HttpSessionStorebounds live MCP sessions withmax_sessions(default10_000); the idle TTL limited how long a session lived, not how many a client could mint. (#289)- The built-in server's
413for a declared over-limitContent-Lengthruns the response phase, so it carries the CORS and security headers the ASGI path already gave it. (#289) StaticFilesstreams a byte range at or aboveSTREAM_THRESHOLD;Range: bytes=0-previously read the whole file into memory. (#289)- The built-in server stops reading a connection once
MAX_PIPELINED_REQUESTS(default64) requests are queued, bounding what a pipelining flood can allocate. (#289) RateLimitMiddleware(max_requests=...)bounds its per-client state withmax_keys(default100_000), matching thestrategy=path. (#289)- An MCP task
ttlis clamped to one hour; a client could otherwise pin a task and its result for the process lifetime. (#289) request.authorizationreports no username for a colon-lessBasicpayload, which RFC 7617 makes malformed; it previously named one for a headerHTTPBasicanswers with a401. (#289)response_modelfilters a msgspec struct orlist[Struct]response as it filters a Pydantic one; it previously shaped nothing on that backend, so a subclass returned under a base-model contract put its extra fields on the wire. (#289)- A credential carrying a non-ASCII byte is refused rather than crashing the request:
decode_jwtraisesInvalidTokenError, a forged CSRF token answers403, and a PKCE verifier is rejected. All three answered500before, pre-authentication. (#289) CORSMiddlewareemitsVary: Originon every response whose allowed origin could depend on the request, including refusals, so a shared cache cannot serve an unkeyed response to an allowed origin. (#289)CSPMiddlewareraisesValueErrorrather than asserting when given no policy, sopython -Ocannot leave it constructed and emitting no header. (#289)- Runtime dependency floors raised to releases carrying current security fixes:
orjson>=3.11.5,pydantic>=2.4.0,python-multipart>=0.0.22,jinja2>=3.1.6,gunicorn>=23.0.0. (#289) - A
dependencies=entry that is not aDependsraisesTypeErrorat registration.dependencies=[guard]- theDepends()wrapper forgotten - was silently discarded, so the guard never ran and every route on it was open. (#289) - The SSE transport runs a tool as the principal that authenticated the
POST, not the one that opened theGETstream. A validated token for one caller executed under another caller's scopes. (#289) - The MCP HTTP endpoint authenticates and
Origin-checksGETandDELETE, not onlyPOST. A resumingGETreplayed another principal's tool output without a credential, and any origin could terminate a session. Clients must now send their token on both verbs. (#289) - A JSON body model refuses a body whose
Content-Typedeclares it is not JSON, closing a CSRF avenue:text/plainand the form types are sent cross-origin without a CORS preflight. An absent header and a+jsonsuffix are still accepted. (#289) await request.json()applies that same rule and reads a declared non-JSON body asNone, closing the avenue for handlers that parse the body themselves. (#289)HTTPExceptioncopies the headers it is given, so a security scheme's cachedWWW-Authenticatechallenge cannot accumulate one request'sSet-Cookieor CORS headers and ship them on the next401. (#289)- A request inside a mounted sub-app reports the connection's scheme and client address;
request.is_securereadFalseover TLS andrequest.client_hostreadNone. (#289) HTTPSRedirectMiddlewareno longer redirects a request that arrived over TLS, which looped when the app served TLS itself or a proxy spelled the scheme in any casing but lowercase. (#289)Request.is_secureisTruefor awssconnection and for any casing of an encrypted scheme;request.schemeandurl_for(_external=True)are normalised to lowercase per RFC 3986 Sec. 3.1. (#289)- The MCP HTTP transport requires and cross-checks
MCP-Protocol-Version,Mcp-MethodandMcp-Nameon the2026-07-28revision. (#289) serve_stdioisolates the protocol wire, so handler or subprocess output cannot corrupt the JSON-RPC stream. (#289)MCPAuthorizationServer.verifier(resource=...)enforces RFC 8707 audience binding, refusing a token minted for another server. (#289)- A registered OAuth client is refused a grant type it did not register for. (#289)
- A route exposed as an MCP tool keeps its rate limit; the replayed call reported no caller, so every call got a fresh bucket. (#289)
@rate_limitis enforced above amax_requests=/window_seconds=limiter; the tag was dropped in silence, leaving the route unthrottled. (#289)- An MCP token minted without a
resourceno longer satisfies a verifier configured withresource=. (#289) - A resuming MCP
GETchecksMcp-Session-Id, so a terminated session cannot replay its buffered payloads. (#289) HTTPBasicCredentialsandHTTPDigestCredentialsmask the password and the digest response in theirrepr. (#289)- A blueprint-scoped
before_requestruns on every route of its blueprint; a dotted route name skipped it. (#289)
Added¶
Veloce.instrumentation_hooksreturns the registered instrumentation hooks in run order, the read halfadd_instrumentationlacked. (#289)URLMapis exported fromveloceand is the public name of the classVeloce.url_mapreturns. (#289)WebSocketStateis exported fromveloce; it is the declared return type ofWebSocket.application_stateand.client_state. (#289)Severityis exported fromveloce; it is the declared type ofFinding.severity. (#289)RateLimitStateis exported fromveloce; aRateLimitStrategyimplementation names it inevaluate. (#289)SignalResultis exported fromveloce; it is the declared return type ofSignal.send. (#289)app.register_auditable(component)reports a non-middleware component toveloce checkandsecurity_audit(). (#289)unregister_converter(name)removes a converter added withregister_converter. (#289)MCPServer(capabilities=[...])serves an out-of-treeCapability;MethodHandleris exported to annotate its handler map. (#289)TestResponseis exported fromveloceand documented; it is what every test-client call returns. (#289)veloce.contrib.mcppublishes all seven capability classes, not three. (#289)app.wait_for_background_tasks(), and the same on both test clients, waits for spawned background work to finish. (#289)MCPServer.capabilitiesexposes the capabilities a server was built with. (#289)SessionMiddlewareBase.wire_cookie_nameexposes the cookie name after the__Host-/__Secure-prefix. (#289)SessionMiddleware.bind_secret_key()settles the signing key from a config before the first request. (#289)app.middlewaresexposes the registered middleware instances in pipeline order. (#289)SessionMiddleware.encode_cookie()/.decode_cookie()sign and verify a session cookie outside a request. (#289)InMemorySessionStoresupportsin, iteration,expires_at()andclear(). (#289)app.iter_routes()returns each route as(method, path, RouteInfo);app.routesremains the six-field summary. (#289)CompressionMiddlewarenegotiates zstd, brotli and gzip fromAccept-Encoding; install thebrotli/zstdextras to offer the newer codings. (#289)- The
ciso8601extra accelerates the{x:datetime}path converter for values without a numeric offset; what matches is unchanged. (#289) RateLimitStrategy.lua_script/lua_argv, an opt-in server-side form a backend can run in place of the Pythonevaluate. (#289)veloce.http.response.header_pop, the replacement half ofheader_get/header_present. (#289)Veloce()warns when an unrecognised keyword looks like a misspelled parameter (tittle=fortitle=), which was previously absorbed intoapp.extrain silence. (#289)veloce checkreports an MCP endpoint mounted withoutauth=or withoutallowed_origins=, asmcp-endpoint-unauthenticatedandmcp-origin-unchecked. (#289)Blueprintacceptstags=andon_duplicate=, the twoRouteroptions it dropped. (#289)StaticFiles(max_age=...)sets the cache lifetime, and the handler honoursSEND_FILE_MAX_AGE_DEFAULTassend_filealready did. (#289)GRACEFUL_DRAIN_TIMEOUTbounds how long shutdown waits for in-flight requests; it was a literal 30 seconds no setting could reach. (#289)Auditablecarries the audit contract for every middleware shape, so aBaseHTTPMiddlewarecan declaresets_hardening_headersand contribute findings. (#289)Converter.specificitydeclares how restrictive a custom converter is, so it can outrankstrduring route matching. (#289)SecurityScheme.openapi_schemepublishes a custom authentication scheme in the OpenAPI document like a built-in. (#289)WEBSOCKET_IDLE_TIMEOUTcloses an idle WebSocket with1001 Going Awayon both transports; it was read only by the built-in server, and is now a documented config key. (#289)TestClient(app, loop=...)drives the app on a loop you supply, which the client never closes. (#289)Middleware.auditlets any middleware contribute findings to the audit, with a severity that decides whether startup refuses to serve. (#289)Finding,AuditContextandAuditFailedcarry a severity, a remedy and a stable id;veloce.audit.run(app)returns them. (#289)SILENCED_AUDIT_IDSdrops named findings, so an accepted one is turned off without turning the audit off. (#289)Middleware.audit_needs_routesskips a route-reading check until the route table is final. (#289)SecurityHeadersMiddlewarereports the opt-in headers it is not sending, with the value to pass. (#289)Middleware.sets_hardening_headersmarks a middleware that adds hardening headers, satisfying the audit's headers check. (#289)SessionMiddlewareBaseis public; subclass it to add a session backend thatsecurity_auditrecognises. (#289)HeaderMismatchErrorrejects a modern MCP request whose standard headers disagree with its body. (#289)veloce checkreports anexclude_middlewarename that matches no registered middleware. (#289)Signal.docrecords the description given toNamespace.signal(name, doc=...). (#289)- A second route taking an existing
name=logs a warning naming both paths. (#289)
Changed¶
- A
tools/callover the HTTP transport is answered as JSON when the tool cannot send a second message, instead of always framing an SSE stream. (#289) - An insufficient-scope
tools/callon a tool answered as JSON returns403with a scope challenge, where a streamed one carries the error in band. (#289) - The built-in server's accept queue follows the machine's
somaxconnrather than asyncio's default of 100. (#289) make_responseanswers a one- or four-element tuple as data; it dropped a four-element tuple's status and headers in silence. (#289)Veloce.make_response,veloce.make_responseand dispatch read one response-tuple table, so a tuple cannot answer three ways. (#289)- A handler may return
(body, header_list); dispatch read the pair list as a status and answered500. (#289) Blueprintrefuses a dot in its own name or a route'sname; the dot separates the blueprint from the route in an endpoint. (#289)make_response(response, status, headers)applies them instead of returning the response untouched; an omitted status isNone, not200. (#289)HTTPBasicCredentialsandHTTPDigestCredentialscompare by identity, so both are hashable again. (#289)- A conditional
GETfor a streamed response answers304; an asset pastFileResponse's streaming threshold was re-sent in full. (#289) - A
304advertises the length the equivalent200would carry, or none when it is unknown, instead of0. (#289) {value:float}is matched before{value:decimal}; float accepts a strict subset, so it is the more restrictive of the pair. (#289)parse_multipart_formis no longer re-exported fromveloce.http.datastructures; import it fromveloce.http. (#289)exclude_middlewareaccepts a middleware class, matched by type so it covers subclasses; a string still matches the resolved name exactly. (#289)exclude_middlewareraisesTypeErrorfor an entry that is neither a middleware class nor a name; such an entry previously matched nothing in silence. (#289)RedisRateLimitBackendruns a built-in strategy as a Lua script - one round trip instead of three, executed atomically, with no contended-key fallback that could admit requests over the limit. A custom strategy keeps theWATCHpath unless it declareslua_script. (#289)View,JSONProvider, the path-converter base and the MCP registry base refuse a subclass that omits a required method, at definition. (#289)CacheandSessionStorerefuse a subclass that omits a required method, at definition rather than on the request that first calls it. (#289)- A traceback frame from a compiled resolver names its handler and shows its source, instead of a bare
<veloce-resolver>with no line. (#289) response_model_*filter flags resolve when the route is registered rather than on every response; assigning one to aRouteInfoafterwards no longer takes effect. (#289)- A compiled resolver converts
str,intandfloatparameters inline instead of calling the shared coercion helper per parameter. (#289) - A response with no background work returns before allocating the task list it used to build and discard. (#289)
- An MCP JSON-RPC envelope is encoded as protocol on every transport, so a custom
json_provider_classno longer injects its keys into protocol frames andJSONIFY_PRETTYPRINT_REGULARno longer inflates each SSE frame. (#289) /openapi.json,/docsand/redocare excluded from the OpenAPI document they serve, so a generated client no longer carries three operations for them. (#289)instance_path=must be a rooted path; a relative one resolved against the working directory the process happened to start in. (#289)- An env file refuses a value that is not the type its config key declares, naming the key.
DEBUG=flaseread asFalse; an empty value still reads as off. (#289) - An MCP
Capabilitydeclares the methods a modern revision retired ashandshake_only_methods; the server derives what it refuses from those instead of a separate table. (#289) - An app-level
url_value_preprocessorruns before a blueprint's, matching the request hooks; registration order no longer interleaves the two. (#289) app.url_value_preprocessorsandapp.url_default_functionskey each blueprint's entries under its dotted name instead of flattening them underNone. (#289)- Three deferred imports of Veloce modules are hoisted to module scope and three are documented with the cycle they break. (#289)
SecurityHeadersMiddlewareapplies its headers with one pass over the response's keys instead of a case-insensitive scan per header, saving 1-4 us per response. Output is unchanged. (#289)APPLICATION_ROOT,MAX_COOKIE_SIZEandPERMANENT_SESSION_LIFETIMEare removed from the config defaults and stop startup when set; passpath=,max_cookie_size=andpermanent_lifetime=to the session middleware. (#289)SessionMiddlewareandServerSessionMiddlewaretake every cookie setting from their constructor;SESSION_COOKIE_NAME,SESSION_COOKIE_SECURE,SESSION_COOKIE_HTTPONLYandSESSION_COOKIE_SAMESITEno longer configure them, and setting one stops startup withAuditFailed. Passsecure=Trueand the rest as arguments. (#289)SECRET_KEYremains the one session setting read from the app; a middleware given nosecret_key=still signs with it. (#289)SessionMiddlewareBase.cookie_lifetimereplaces the private_cookie_lifetime; a subclass calling the old name must rename it. (#289)security_auditasks each registered middleware for its findings instead of naming middleware classes, so a middleware written outside Veloce is audited like a built-in. (#289)- Startup runs the full audit and refuses to serve on an
errorfinding, raisingAuditFailed;RateLimitMiddlewareraisedValueErrorfor the same case, whichAuditFailedstill is. (#289) veloce checklabels each line with its severity and exits 0 when onlyinfofindings are present. (#289)- A nested blueprint's hooks and URL processors run only on that blueprint's own routes, not on every route under its parent. Register a guard on the parent blueprint to keep app-wide coverage. (#289)
ServerSessionMiddlewarehonourssession.permanent, so a permanent session's cookie and store entry both usepermanent_lifetime=. Sessions that previously expired at the 14-day default now live as long as that argument says. (#289)security_audit()warns about a non-Securesession cookie for any session middleware, so an app usingServerSessionMiddlewaremay see a warningveloce checkdid not previously report. (#289)- Malformed JSON in a body model follows the same policy as
request.json(): a400with a stable message, and the decoder's reason only underJSON_ERRORS_VERBOSEor debug. It was a422with a generic message, so the opt-in did nothing there. (#289) run(access_log=True)writes a per-request access line; it previously printed only the startup banner. The ASGI path is unchanged, where the server already logs. (#289)- A
boolquery, path, header or cookie parameter accepts Pydantic's spellings (on/off,t/f,y/nas well astrue/false,1/0,yes/no) and refuses anything else with a422; it previously read every unrecognised value, includingonand any typo, asFalse. (#289) log_exceptiontakes an optionalrequest=and names it in the record. (#289)- Query strings and urlencoded bodies carrying no percent-escape skip per-field decoding. Measured on one Windows desktop: 20.9 vs 26.0 us for a three-parameter query read, 35.3 vs 43.4 us for a five-field form POST; an escaped value is unchanged. (#289)
veloce.__version__reads the package metadata on first access instead of at import. Measured on one Windows desktop: about 14 ms off a ~342 ms cold start. (#289)- A cookie value is percent-decoded only when it carries an escape or a quote. Measured on one Windows desktop: 1.84 vs 3.03 us for a four-cookie header. (#289)
Content-Typeparsing skips the parameter walk when the header declares no parameters. (#289)- The
MAX_CONTENT_LENGTHheader scan matches the name as ASGI mandates it rather than lowercasing every header. A server that sends a differently-casedContent-Lengthloses the early rejection, not the limit. (#289) TestClientbuilds aSelectorEventLoopon Windows; passloop=asyncio.ProactorEventLoop()for a handler that spawns a subprocess. Measured on one Windows desktop: 73.0 vs 93.6 us per request. (#289)- An ASGI WebSocket message is read and written one coroutine frame deep when no timeout is set. Measured on one Windows desktop: 1.34 vs 2.02 us per echo round. (#289)
- An MCP
list[T]tool argument refuses a non-array and a wrong-typed member instead of wrapping or passing it through; send the array the published schema declares. (#289) from_prefixed_envcoerces a value to its key's declared type and refuses one it cannot convert. (#289)Blueprint.errorhandlerraisesTypeErrorfor a key that is neither a status code nor an exception class. (#289)- The MCP transport trims a bearer token as RFC 7235 allows; a token carrying other whitespace is now refused. (#289)
HttpSessionStore.resolveno longer scans every live session, so MCP request cost stays flat under load. (#289)@app.middleware("http")raisesTypeErrorfor options it cannot honour instead of dropping them. (#289)- A
MethodViewverb method declaring a parameter marker orDepends()raisesTypeErrorat class definition. (#289) Config.from_mappingraisesTypeErrorfor a non-uppercase keyword argument instead of dropping it. (#289)- Route registration no longer computes a dependency-grouping map that nothing read. (#289)
- An MCP server-to-client request is refused when the client advertised the capability as
false. (#289) register_blueprintraisesValueErrorfor a different blueprint under a name already registered; give one a different name. (#289)InMemorySessionStoresupportslen(), so an empty store is now falsy; teststore is not Noneto mean a store is configured. (#289)jsonable_encoderappliesincludeas a key whitelist at every depth; list a nesting key or its branch is dropped. (#289)
Removed¶
Veloce.use_secure_defaults(); registerSecurityHeadersMiddleware(hsts_max_age=31536000)and passsecure=Trueto the session middleware. (#289)veloce.routing.paramsis removed; import the markers fromveloceorveloce.routing. (#289)
Fixed¶
- A streamed file sends no more than the
Content-Lengthit declared when the file grows mid-response. (#289) - A
list-typedHeader()orCookie()resolves on a websocket route instead of closing the handshake1011. (#289) /openapi.jsongives a request schema its own component when a nested model is also returned by another route. (#289)- The derived-model cache for
response_model_include/_excludekeys on the model, not its address. (#289) - The MCP HTTP transport publishes the session from its SSE reply path, so a conformant client's handshake is recorded. (#289)
- A
@rate_limit-tagged route's backend honours the middleware'smax_keys. (#289) - The MCP stdio ordering wait no longer absorbs a cancel delivered to its own task. (#289)
- An MCP server-to-client request whose emit fails leaves no entry in the pending correlation table. (#289)
- A grouped parameter's schema walk reports failure instead of silently publishing the field without its
ge/le/title. (#289) jsonable_encoderappliesexcludebelow a model, not only to its own fields. (#289)Query(default=[])and other mutable marker defaults are copied per request. (#289)client.session_transaction()applies the middleware's own age ceiling, so a cookie a request would refuse no longer loads. (#289)- A status-code error handler taking
(request, exc)is called correctly on the unhandled-exception and405paths; it raisedTypeErrorout of dispatch. (#289) SessionAuthdescribes itself in the OpenAPI document, so a session-guarded route declares a security requirement instead of publishing as open. (#289)- The legacy MCP SSE transport sends its reconnect hint on the first frame; no frame carried
retrybefore. (#289) - A header parameter containing a backslash is quoted and escaped, so a
Content-Dispositionfilename ending in one no longer emits an unterminated quoted-string. (#289) - The exported
http_exception_handlerrenders the same body as the default error path: an empty detail is not replaced with"Error", and a body-limit refusal keeps itslimit. (#289) AcceptHeader.best_matchhonours an explicitq=0on a non-MIME header, soAccept-Encoding: gzip;q=0, *no longer selectsgzip. Affectsrequest.accept_encodingsandrequest.accept_languages. (#289)veloce.make_responsereturns aResponseargument unchanged; it previously JSON-encoded the object into its own repr. (#289)veloce.make_responsetypes abytesbody astext/html, matchingVeloce.make_responseand dispatch. (#289)- The startup banner prints the installed framework version; it printed the app's
version=argument, defaulting to0.1.0. (#289) - A
TestClientconstructor error surfaces instead of being buried by anAttributeErrorfrom the finaliser. (#289) - The declared
Content-Lengthcheck is skipped for methods that carry no body; the received-length cap is unchanged and still refuses an over-limit body on any method. (#289) WebSocket.send_json(mode="text")frames the encoded payload directly on the built-in server instead of decoding and re-encoding it. (#289)CompressionMiddlewarememoises the negotiated coding perAccept-Encodingvalue, bounded at 256 entries. (#289)- Generated resolver source listings registered for tracebacks are capped, bounding a process that registers routes over its lifetime. (#289)
- An after-request hook or error handler that cannot be weakly referenced - a slotted callable, a method descriptor - runs instead of answering
500. (#289) - A validation error reports an array index in
locas an integer on every body path, matching the publishedValidationErrorschema. (#289) jsonable_encoderappliesexclude_unsetandexclude_defaultsto a nested model, not only to one passed in directly. (#289)jsonable_encoderappliesexclude_noneto an arbitrary object's attributes, matching every other branch. (#289)response_model_include/response_model_excludeshape the OpenAPI response schema, so the document names exactly the fields the route sends. (#289)Response.check_preconditionsenforcesIf-Unmodified-SincealongsideIf-Match, in RFC 9110 precedence; a date-based precondition was previously ignored. (#289)- An exception handler declared
def handler(**kwargs)receivesrequestandexc; it was called with an empty mapping. (#289) - A dataclass or
TypedDictreturn annotation declares a response contract, so the return is filtered rather than served whole. (#289) response_model=<dataclass>filters a value of a different dataclass instead of raising; it previously answered500. (#289)- A tuple return that is not
(body, status[, headers])reads the same with and without aresponse_class; theresponse_classpath took the first element and discarded the rest. (#289) - A parameter declared on a dependency is published in the OpenAPI document; it was enforced with a
422but absent from the schema. (#289) - A tool declared with
@app.mcp_toolrenders its result in the app's JSON dialect, as a route-exposed tool already did; the two disagreed and the route-backed one only matched by accident. (#289) CORSMiddlewarekeeps anAccess-Control-Expose-Headersentry another middleware contributed under any casing; it checked two spellings and silently discarded the rest. (#289)- Replacing
AlloworContent-Lengthclears the existing header whatever casing it was stored under, so a response cannot carry two. (#289) app.openapi_versionis emitted in the generated document; it was documented as the spec version the document carries and was read nowhere. (#289)- The OpenAPI document and the MCP
initializeresult report the same application title; the two builders carried different fallback defaults. (#289) - A pre-dispatch refusal (the 413 on both transports, and the ASGI 400) is encoded in the app's JSON dialect. These ran before the app contextvar was bound, so the dialect appeared only when an earlier request had left it set on the same task. (#289)
app.run()answers aMAX_CONTENT_LENGTHrefusal with the same JSON body as the ASGI path; it sentContent Too Largeas untyped text, so a client parsing the documented error shape failed on that transport only. (#289)titleandversionmust be non-empty strings; a non-string produced an invalid OpenAPI document and a 500 on/docs, andvalidate_openapi=Truedid not catch it. (#289)- An
exception_handlers=key that is not an int status code or an exception class raisesTypeError. A string key was stored in a table matched by MRO walk, so the handler never fired. (#289) docs_url=""andredoc_url=""disable that page instead of mounting it at the site root, and two documentation pages sharing a path are refused at construction rather than on a later request. (#289)- A text
response_classgiven adictorlistraisesTypeErrornaming the class and the remedy, instead ofAttributeError: 'dict' object has no attribute 'encode'. (#289) /docsand/redocpoint at the schema path the app actually serves, includingprefix=androot_path. Both rendered empty on a prefixed app, and ReDoc had no way to override it. (#289)mount_mcp(transport="sse", auth=...)serves the RFC 9728 protected-resource metadata its401challenge points at; the route was registered by the HTTP transport alone. (#289)- The MCP stdio transport encodes a reply with the same fallback the HTTP path uses, and answers
-32603rather than writing nothing when a value cannot be serialised. ADecimalinctx.result_metahung the client. (#289) - An MCP request naming a handshake-era revision in
_metais served as handshake-era by both the transport and the core, so it no longer skips the standard-header cross-check while being answered in the modern envelope. (#289) - A mounted sub-app that cannot serve a request raises instead of silently falling through to the next mount with the body already drained. (#289)
- A blueprint
url_value_preprocessorno longer runs on every request nor costs every route in the app its straight-line dispatch. (#289) Veloce.process_responseruns the dispatch path, so a hook declaring onlyresponseno longer raises, a non-Responsereturn no longer replaces the response, andafter_this_requestcallbacks run. (#289)Router(tags=[...])copies the list instead of appending route tags to the caller's own. (#289)TestClient.cookiesandTestResponse.cookiesreport the decoded value the handler receives, not the percent-encoded wire form. (#289)- A route whose
response_model=disagrees with its return annotation failsveloce check; it was printed and the command exited 0. (#289) - Response-contract findings carry ids, so
SILENCED_AUDIT_IDSreaches them, and are reported by severity rather than only underdebug. (#289) MCP_CALL_TIMEOUT,MCP_ENFORCE_LIFECYCLE,MCP_RESOURCE_SUBSCRIPTIONSandEVENT_LOOP_WATCHDOGare declared config keys, so an env-file value gets the right type;MCP_CALL_TIMEOUT=5reachedasyncio.wait_foras a string and broke every tool call. (#289)- The configuration guide documents every shipped key, and a test fails when the two drift apart. (#289)
app.mount()accepts any handler exposingprefixandhandle(request), not only aStaticFiles. (#289)- A subdomain route no longer matches an IP-literal host, where the router and
request.subdomaindisagreed. (#289) - An instrumentation hook marked
is_access_logsuppresses the built-in access log, sorun(access_log=True)does not log twice. (#289) TestClientrestores the app's setup lock on close instead of leaving it disabled. (#289)session_transaction()seeds the cookie under the name the middleware reads, so acookie_prefix=session is found instead of silently running anonymous. (#289)session_transaction()works before the first request when the signing key comes fromapp.config, and explains why a server-side backend cannot be seeded. (#289)Veloce(root_path=...)reachesrequest.root_path,script_root, externalurl_forand slash redirects; it was stored and read by nothing. (#289)- A blueprint mounted at two prefixes runs its hooks and URL processors once per request, not once per mount. (#289)
- Silencing
ratelimit-overrides-unknownno longer turns a startup refusal into a 500 on every request; the request path reports and the audit decides. (#289) security_auditwalks dispatch-shape middleware, ASGI middleware classes and static handlers, not onlyMiddlewareinstances; a correctly hardened app was reported as unhardened. (#289)- A
StaticFilesdirectory that does not exist is reported byveloce check; it previously only warned at construction. (#289) security_auditno longer warns thatSECRET_KEYis unset for a session middleware constructed with its ownsecret_key=. (#289)- A session middleware with no signing key from either source refuses startup instead of raising on the first request. (#289)
JSONResponseand a bare mapping yielded toEventSourceResponsehonourJSON_SORT_KEYSand a custom JSON provider; both encoded directly and missed the app's dialect. (#289)- A
date,datetime,time,timedelta,decimal,any(...)or custom converter outranksstrwhatever the declaration order; which route answered depended on which was declared first. (#289) - A route guarded by a custom
SecuritySchemeis published with its security requirement; the document asserted the route was open. (#289) - A scheme that cannot describe itself warns during the schema build instead of silently publishing the route as unauthenticated. (#289)
from_env_filegives a value the type its config key is read as, soMAX_CONTENT_LENGTH=1000no longer raisesTypeErroron every request with a body. (#289)DEBUG=false,JSON_SORT_KEYS=falseandTCP_KEEPALIVE=falsein an env file read as off; a non-empty string was truthy, so they read as on. (#289)SILENCED_AUDIT_IDSfrom an env file splits on commas; left a string, a membership test matched single characters. (#289)- A blueprint's
before_request,after_requestandteardown_requestrun on routes of a nested blueprint that declares none of its own; a guard on a parent blueprint was skipped there. (#289) security_auditno longer warns about a session middleware constructed with an explicitsecure=True; it read onlySESSION_COOKIE_SECURE. (#289)- The
Connectionheader states what the built-in server actually did: an HTTP/1.0 request, one asking forConnection: close, and a native SSE stream were all answeredkeep-aliveon a socket the server then closed.EventSourceResponseno longer setsConnectionas a response header. (#289) - A
StreamingResponsewith a bodiless status (204,205,304) sends no chunks and advertises noTransfer-Encoding, which RFC 9112 Sec. 6.1 forbids there; it previously desynchronised keep-alive connections. (#289) - Assigning
response.bodyrefreshesContent-Length, asset_dataalready did; a middleware rewriting a body advertised the previous length. (#289) - A
HEADon anEventSourceResponseadvertises the chunked framing itsGETuses instead ofContent-Length: 0. (#289) - Every
Responseheader accessor reads the header under any casing;vary,allow,cache_control,date,location,ageand seven others saw only the canonical spelling. (#289) response.expires = Noneandlast_modified = Noneremove the header whatever casing it was stored under; a third spelling survived while the getter reported the clear had worked. (#289)- The built-in server percent-decodes the request path, as an ASGI server does, so
/items/a%20bbinds"a b"on both; it bound the raw"a%20b", and a handler saw a different value depending only on how the app was served. (#289) veloce.make_responseunpacks a(body, status)/(body, status, headers)tuple asapp.make_responseand a handler return already did; it JSON-encoded the whole tuple, status code included, into a200body. (#289)app.make_responsecoerces any value rather than raisingTypeErroron123orNone, matching what a handler returning the same value is answered. (#289)- A file above the 64 KiB inline threshold is streamed off disk instead of read whole, so resident memory no longer scales with file size times concurrent downloads. It still advertises
Content-Length;FileResponse.bodyis empty for such a response and the bytes arrive as chunks. (#289) - A
stream=Trueroute no longer retains the body it streams: a 32 MiB upload left 33.6 MB resident after the handler had consumed it, so the flag bounded nothing. A streamed body is consume-once, andrequest.body()afterwards reports empty. (#289) - A
stream=Trueroute keeps streaming when its app is mounted; the mounted dispatch buffered the whole body first, so the flag silently stopped applying under composition. (#289) - A
samesitevalue is normalised once, inside theSet-Cookieserialiser; a whitespace-only value made the cookie-backed session raise on every response while the server-side one silently omitted the attribute. (#289) url_forrefuses a/in a value bound to a segment-bounded path converter instead of returning a URL its own router answers404for; apathplaceholder still accepts slashes. (#289)request.subdomainreads the host through the framework's shared reader, so an IPv6 literal is not split on its first colon, and an IP literal - v4 or v6 - reports no subdomain rather than an address label. (#289)- A
{v:float}placeholder on a regex-fallback route accepts+1.5,.5and5., which the radix tree already did; the fallback's pattern was stricter than the converter and refused them first. (#289) - A blueprint route declared
strict_slashes=Falsekeeps it through registration; the same route reached throughinclude_routerkept it, so one declaration behaved two ways. (#289) - A plain mutable parameter default (
tags: list = []) is fresh on every MCP tool call, as it already was on every HTTP request; one call's mutations reached the next. (#289) WebSocket.close()clamps its reason to the RFC 6455 control-frame budget on both transports and never puts a reserved close code (1005,1006,1015) on the wire; an over-long reason previously reached the peer as an abnormal1006under an ASGI server. (#289)- A refused WebSocket handshake applies the host/Origin allow-lists before matching the route on both transports, so a refused origin cannot learn which paths exist; the built-in server matched first and answered
404for an unknown one. (#289) - An ASGI WebSocket handshake for an unregistered path answers
404where the server advertises thewebsocket.http.responseextension, instead of a1008close indistinguishable from a policy refusal. (#289) - An over-
MAX_CONTENT_LENGTHrejection runs the response phase and states one message on every route kind; the ASGI refusal was written before aRequestexisted, so it carried none of the app's response headers and worded itself differently from astream=Trueroute's. (#289) MAX_FORM_PARTS = Nonelifts the field cap for a multipart body as it already did for a urlencoded one; multipart kept the built-in 1000-part limit whatever the setting said. (#289)- An MCP tool argument declared inside a
Dependsis held to the type the tool'sinputSchemapublishes, as a top-level argument already was;"yes","1"and1were read astruefor a declaredboolbehind a dependency. (#289) MCPContext.list_resourcesandlist_promptsomit what the connection hid withhide(), matching whatresources/listandprompts/listreport; they are unpaged, since a handler cannot ask again for the next page. (#289)- A closed legacy MCP SSE stream reclaims its session, so an unsettled task and its runner no longer outlive the connection for the process lifetime. (#289)
- The legacy MCP SSE transport distinguishes an unreadable body (
-32700) from a readable one of the wrong shape (-32600), as the other two transports do; it answered-32603for both. (#289) - An MCP request naming a handshake-era revision in
_metais served instead of refused for a header that revision never defined. (#289) describe_toolsadvertises the modern tool shape to a modern client; undertool_searchit is the only definition such a client sees. (#289)tasks/canceland the task status notification spell the duration fields the way the caller's revision does, matching creation and polling. (#289)- A WebSocket handler's exception is reported on the built-in server; a peer that left cancelled the close handshake and the failure was lost. (#289)
- An unhandled exception is logged with its traceback and the failing request instead of vanishing into a generic
500. (#289) - A quoted
Content-Typeparameter containing;is no longer cut short:profile="a;b"was read as"a. The same applies to theAcceptmedia-range key. (#289) - A quoted header parameter written with whitespace before the opening quote (
filename = "r.pdf") no longer keeps that space in its value. (#289) - The deployment guide's deprecation section named the wrong warning category, so the command it gave caught nothing. (#289)
veloce --versionreports the same fallback asveloce.__version__when the package metadata cannot be read; it claimed0.3.0. (#289)stream=Trueis honored on a blueprint route and on an included router's route. (#289)- A blueprint route keeps its MCP resource mime type, size, annotations and
_meta. (#289) - Capabilities are advertised per protocol revision, so none is offered and then refused. (#289)
tools/listomitsexecutionfor a modern client, whose revision removed the field. (#289)server/discoveris marked private rather than publicly cacheable; its answer varies by caller. (#289)subscriptions/listenworks on the default HTTP deployment instead of requiring a persistent session. (#289)Query(group=True)and its header and cookie forms appear in the OpenAPI document and in MCP tool schemas. (#289)- An MCP tool call binds a grouped model instead of failing with an internal error. (#289)
- A grouped field's declared constraints reach both published contracts instead of only the runtime. (#289)
gkeeps a value written by a sync handler or an offloaded dependency. (#289)MCPContext.result_metawritten by a sync tool reaches the client. (#289)Response.add_varymerges an existingVarystored under any header casing instead of dropping it. (#289)- Clearing
location,date,age, and six sibling response headers works under any stored casing. (#289) GZipMiddlewarehonours aQ=0refusal, not only the lower-case spelling. (#289)- The MCP transport does not send an SSE stream to a client that refused it with
q=0. (#289) RedisRateLimitBackendfalls back to optimistic locking on a server with scripting disabled. (#289)- Dynamic client registration stores the requested
grant_typesand echoes what it stored. (#289) HTTPBearer(scheme_name=...)publishes that scheme in the OpenAPI document, not a fixedbearer. (#289)handle_http_exceptionrenders the same body as the request cycle for an exception with an empty detail. (#289)response_model=Sequence[Model],tuple[Model, ...]andset[Model]document an array of refs. (#289)- The
HTTPValidationErrorschema declares thestatus_codeevery 422 response carries. (#289) Body()on a model parameter validates it, instead of passing the raw decoded body to the handler. (#289)- An MCP tool schema publishes a parameter marker's constraints, matching the OpenAPI document. (#289)
CORSMiddleware's usage example listsallow_headers, which credentials require. (#289)- Mounting an MCP transport twice no longer leaves the first mount unreachable by
url_for. (#289) - Fix the order of grouped lifespan-teardown failures; an expanded exception group's members were reported backwards. (#289)
request.is_disconnected()reportsTrueon the built-in server when a client vanishes mid-body; the disconnect was signalled as an ordinary end-of-body. (#289)veloce mcp run --transport httpsays which server it fell back to when uvicorn is absent, asveloce runalready did. (#289)
[0.17.1] - 2026-08-23¶
Added¶
- Benchmarks page: measured throughput against other frameworks, and the method behind the numbers. (#286)
Changed¶
GZipMiddlewarecompresses a buffered body belowmin_stream_chunk_offloadinline instead of on the thread pool. Measured at 32 concurrent requests: 12,489 vs 5,393 requests per second for a 6 KiB body, 5,432 vs 4,102 at 32 KiB; past roughly 48 KiB the pool wins and is still used. (#286)
[0.17.0] - 2026-08-23¶
Added¶
VeloceDeprecationWarningcarries every Veloce deprecation and is visible under the default warning filter. (#284)url_foris importable from the top level, building a URL against the active app. (#284)UploadFile.save_asyncstreams an upload to disk without blocking the event loop. (#284)WebSocket.accepted_subprotocolreports the subprotocol the connection settled on. (#284)Capability,Transport,BidirectionalTransportandregister_sse_transportare exported fromveloce.contrib.mcp. (#284)MCPContextexposesclient_id,request_id,task_id,origin_request_id,transportandlifespan_context. (#284)
Fixed¶
TrustedHostMiddleware,HTTPSRedirectMiddlewareandCSRFMiddlewarestand down for a replayed MCP call, which they previously refused. (#284)add_middleware(instance, name="x")applies the name, soexclude_middleware=["x"]matches it. (#284)- A class-based view receives its path parameters;
MethodView.get(self, request, uid)no longer raises. (#284) add_url_ruleregisters the verbs aViewdeclares instead of defaulting toGET. (#284)HTTPSRedirectMiddlewareignores anX-Forwarded-Protohop thatProxyFixrefused. (#284)render_template_stringresolves filters, globals and tests registered on the app. (#284)TestClient.websocket_connectsends aHostheader, which RFC 6455 Sec. 4.1 requires. (#284)- A conditional 304 advertises the representation's length instead of
Content-Length: 0, including when a handler and a middleware both downgrade it. (#284) - An
after_requesthook is called by its own signature, so one taking only(response)works. (#284) - Task augmentation is refused on every method that cannot run in the background, not just two. (#284)
veloce checkandveloce routesload the dotenv file, and accept--env-file/--no-env-file. (#284)CORSMiddlewaremergesAccess-Control-Expose-Headersinstead of discarding another middleware's entries. (#284)BadResetTokenis raised on misuse; it also subclassesTypeError, which was raised before. (#284)Request.is_disconnected()reports a real disconnect on astream=Trueroute. (#284)MAX_CONCURRENT_CONNECTIONSandWRITE_BUFFER_HIGH_WATERare seeded indefault_config(). (#284)- A multipart body that ends inside a part is refused with 400 instead of returning 200 with the field missing. (#284)
- A multipart upload's spool file is closed once the request is done with it, instead of surviving until collection. (#284)
Request.url_for(..., _external=True)builds from the request's recovered scheme, host, port andscript_root. (#284)
Deprecated¶
Veloce.on_event()warns throughVeloceDeprecationWarning; use@app.on_startup/@app.on_shutdown. Removal in v1.0.0. (#284)FileResponse(path)on a running loop warns throughVeloceDeprecationWarning; useawait FileResponse.from_path(path). (#284)
Changed¶
- Deprecation warnings are raised as
VeloceDeprecationWarningrather thanDeprecationWarning, which the default filter hid. (#284) import veloceno longer imports the MCP, OpenAPI or Redis integrations; they resolve on first use. (#284)MAX_CONTENT_LENGTHis enforced once per request by the transport that read the body, instead of again during dispatch. (#284)add_middlewareraisesTypeErroron a construction argument passed with an already-built instance, instead of dropping it. (#284)- The templating error names
Veloce(template_folder=...)rather than a private attribute. (#284) url_for,url_path_forandRequest.url_fortake the endpoint positionally, so a route may have a{name}segment. (#284)CORSMiddlewaresendsAllow-CredentialsandExpose-Headersonly when an origin was allowed. (#284)SessionAuthlets a missingSessionMiddlewaresurface instead of masking it as an anonymous request. (#284)TestClientpercent-decodes the request path, as an ASGI server does. (#284)ToolSearchtools are counted by the MCP scoped-tool scan. (#284)- A class-based view is forwarded only the path parameters its target declares; reading
request.path_paramsstill works. (#284)
[0.16.0] - 2026-08-23¶
Security¶
- Replaying an MCP refresh token revokes the whole token family, per OAuth 2.1 Sec. 4.14.2. (#282)
- A trusted
Forwardedheader is the sole authority forfor,protoandhost; a hop refused by trust depth can no longer set them throughX-Forwarded-*. (#282) RateLimitMiddlewarekeys on the caller's address under ASGI; a changingUser-Agentno longer bypasses it. (#282)
Added¶
MCPContext.request_metareads the_metathe client sent with this request. (#282)add_mcp_proxy(scopes=..., tags=...)puts an upstream's tools behind a scope or a label. (#282)- The 26 named HTTP exception classes are exported from the top level. (#282)
VeloceErrorroots every exception Veloce raises; every existing base is kept. (#282)QueryParams,Cookies,StateandAddressare exported from the top level. (#282)- The ten signals are exported from the top level. (#282)
HealthPluginis exported from the top level. (#282)ServerNotImplementednames the 501 exception class;NotImplemented_still resolves to it. (#282)- A resource template accepts
{+name}, binding a whole path - separators included - to one variable. (#282) veloce mcp runserves an app's MCP tools, which is what a client config file launches. (#282)veloce mcp listprints the tools, resources and prompts a client would see. (#282)MCPAuthorizationServerissues MCP tokens: OAuth 2.1 with PKCE, refresh rotation, and RFC 7591 registration. (#282)register_authorization_servermounts its metadata,/authorize,/tokenand/register. (#282)AuthorizationStoreandInMemoryAuthorizationStoreback the issued clients, codes and tokens. (#282)mount_mcp(transport="sse")serves the deprecated split-endpoint SSE wire. (#282)mount_mcp(session_backend=...)shares HTTP MCP sessions between workers. (#282)SessionBackendandSessionRecordare exported for implementing that store. (#282)mount_mcp(page_size=...)paginates the MCP list methods with the spec's opaque cursor. (#282)- A tool may return a list of content blocks, emitted in order as the result's
content. (#282) MCPContext.statereaches the call's request state, so a handler holding the context can stash a value. (#282)mcp_tool(tags=...)labels a tool, and every tool exposestagsfor a visibility policy. (#282)- A
pathlib.Pathparameter declaresformat: pathin the tool schema. (#282) mcp_resource_mime_type=declares the media type a resource listing advertises. (#282)mount_mcp(tool_search=...)publishessearch_tools,describe_toolsandrun_toolsin place of the catalogue. (#282)run_toolsruns several declared calls in one request, passing results between steps. (#282)mcp_tool(version=...)registers several versions of a tool under one published name. (#282)MCPContext.sample_with_toolsruns the sampling loop, executing the tools the model asks for. (#282)SamplingRunandSampledToolCallreport a run's answer, transcript and tool calls. (#282)derive_toolandArgTransformpublish a narrower façade over a registered tool. (#282)app.add_mcp_toolregisters an already-built tool. (#282)MCPContext.hide/unhide/reset_visibilitynarrow one connection's view of the catalogue. (#282)app.mount(..., expose_mcp=True)publishes a sub-application's MCP primitives through its parent. (#282)add_mcp_proxyserves another MCP server's tools from this app, forwarding each call. (#282)@app.before_mcp_calland@app.after_mcp_callrun around every MCP call, route-backed or not. (#282)MCPContext.result_metaattaches_metato the result of the call being handled. (#282)meta=onmcp_tool/mcp_promptandmcp_meta=on a route publish_metaon the definition. (#282)mcp_resource_size=andmcp_resource_annotations=declare what a resource listing advertises. (#282)Veloce(website_url=..., mcp_icons=...)publishes them in the MCPserverInfo. (#282)MCPContext.sample(include_context=...)asks the client to attach server context to the prompt. (#282)mcp_tool(annotations=...)declares the behaviour hints a tool with no HTTP verb cannot derive. (#282)render_template,render_template_stringandstream_templateare exported fromveloce.contrib. (#283)MCPRequestErroris exported fromveloce.contrib.mcp.transports. (#283)mount_mcp(tool_filter=...)narrows which toolstools/listreports per caller. (#282)MCPContextreportssession_id,client_info,client_capabilitiesandis_background_task. (#282)MCPContext.client_supports(name)tests an advertised client capability, nested with dots. (#282)MCPContext.debug/info/warning/errorare shorthands for the matchingloglevel. (#282)MCPContext.read_resourceandget_promptreach the server's own components, scope checks included. (#282)MCPContext.list_resourcesandlist_promptsenumerate what the list methods report. (#282)MCPContext.send_notificationsends an arbitrary JSON-RPC notification to the client. (#282)- Cacheable MCP results carry
ttlMsandcacheScopeon the modern revision. (#282) mount_mcp(cache_ttl_ms=...)sets the freshness hint sent with those results. (#282)subscriptions/listenopens a filtered notification stream, replacingresources/subscribe. (#282)notify_tools_list_changed()andnotify_prompts_list_changed()signal those lists changed. (#282)- MCP tasks are served as the
io.modelcontextprotocol/tasksextension on the modern revision. (#282) tasks/updatedelivers responses to a task's outstanding input requests. (#282)
Changed¶
- The built-in server buffers a route's body before dispatch unless it declares
stream=True; declare the flag to keep incremental delivery. (#282) app.mount(..., expose_mcp=True)takes its flag as a keyword; passing it positionally is refused. (#282)- A stateful connection advertises
listChanged: truefor tools, prompts and resources. (#282) - A tool, prompt or resource whose declared
scopesthe caller lacks is no longer listed. (#282) - A resource list narrowed by declared scopes is marked private, so a shared proxy cannot reuse it. (#282)
resources/readandprompts/getrefuse a task-augmented request instead of answering synchronously. (#282)- A modern client must declare the tasks extension before a task handle is returned. (#282)
tasks/listandtasks/resultare not served to a modern client;tasks/getcarries the result. (#282)pingandlogging/setLevelare not served to a modern client; both revisions keep their own surface. (#282)- A modern client sets its log level per request via
_meta; a request naming none receives no log notifications. (#282) tools/list,prompts/listandresources/listbuild each entry once and reuse it. (#283)veloce.appexportsVeloce,URLRuleandPlugin;import *no longer pulls in stdlib names. (#283)- A test-client websocket read raises
RuntimeError, not bareException, when the peer closes. (#283)
Fixed¶
- A
typing_extensions.TypedDictis recognised as an object shape; it was previously advertised as a string. (#282) - A
TypedDictPydantic cannot adapt on this interpreter falls back to a plain mapping instead of failing the request. (#282) .js,.json,.css,.svgand.wasmare served with their standard media type regardless of the host registry. (#282)- The gunicorn worker warns when its TLS certificate is expired or not yet valid. (#282)
- A dropped SSE client no longer leaves the in-flight call buffering notifications nobody reads. (#282)
- Closing a stdio MCP connection reclaims the tasks it created, including one that never settles. (#282)
- The gunicorn worker honours
--ssl-versionas a minimum TLS version; a floor below the interpreter default is refused and logged. (#282) request.schemereportshttpson a TLS connection served by the built-in server or the gunicorn worker. (#282)X-Forwarded-Protono longer sets the scheme from a hopProxyFixrefused. (#282)- A task-augmented
tools/callis refused on a connection with no session, which previously pinned an unreachable task. (#282) MCPContext.session_idis unique across worker processes, so per-client state is no longer shared between unrelated clients. (#282)- Graceful shutdown closes idle keep-alive connections before awaiting the server, so shutdown hooks run instead of being killed. (#282)
request.get_json()andrequest.dataread the body under the built-in server and the gunicorn worker. (#282)- A response's ETag,
Last-Modified,ExpiresandVaryare read whatever casing wrote them. (#282) - An
If-Matcha response satisfies is no longer refused when its ETag was written asEtag. (#282) FileResponsenames its media type through the same memoized lookup the static server uses. (#282)StaticFileshonours anIf-RangeETag a subclass emitted with surrounding whitespace. (#282)- A proxied call forwards the caller's
_meta, so an upstream sees the progress token. (#282) run_toolsrefuses a plan whose step ids repeat instead of mis-resolving a$fromreference. (#282)- A
$frompointer of/names the member keyed"", as RFC 6901 defines it. (#282) - An array index in a
$frompointer is refused unless it is unsigned and unpadded. (#282) - A too-deeply-nested step argument fails that step rather than the whole plan. (#282)
- A proxied result whose content block is not an object no longer ends the plan. (#282)
SamplingRun.messagesends with the answer, so extending it for another run keeps it. (#282)- A sampled tool's
structuredContentreaches the model instead of only its text. (#282) - A fractional number is refused where a tool declares an integer, instead of losing its fraction. (#282)
- A number or a string is refused where a tool declares a boolean. (#282)
derive_toolrefuses to derive from a derived tool, which published a surface no call could satisfy. (#282)ArgTransform(schema=...)refuses a type the handler behind it would reject. (#282)- A copied tool no longer advertises versions only the tool it was copied from can serve. (#282)
- A proxied tool drops the upstream's version metadata, which the gateway cannot honour. (#282)
- A listing a connection can narrow is marked
private, so a shared cache cannot replay it. (#282) MCPContext.hidenarrows whatsearch_toolsanddescribe_toolsreport, not only the listing. (#282)- A server that can narrow nothing no longer rebuilds its catalogue on every discovery call. (#282)
- Every
MCPErrora tool handler raises reaches the caller with its code, message anddata. (#282) - A route-backed tool's
MCPErroris delivered instead of being rendered as an HTTP error body. (#282) MCPContext.hideannounces only the listing the hidden name belongs to. (#282)- A
list_changednotification is no longer sent for a capabilityinitializedid not advertise. (#282) - An MCP tool result encodes through the framework's own encoder, so both doors answer the same JSON. (#282)
- A
Secretin a tool result is refused, as it already was on the HTTP path, instead of being emitted. (#282) - A model's computed fields survive the
orjsonfallback, matchingjsonable_encoder. (#282) - A
msgspec.Structpublishes its fields instead of its Python repr. (#282) - The MCP protected-resource metadata advertises
bearer_methods_supported. (#282) - A
list[Model]tool parameter publishes the model as its item schema, not a string. (#282) - An MCP argument whose JSON type contradicts the published schema is refused, not passed to the handler. (#282)
- A JSON-RPC response POSTed to the HTTP transport is accepted with
202, not refused. (#282) - URL-mode elicitation sends the required
elicitationId, so a conforming client accepts it. (#282) - URL-mode elicitation is refused unless the client declared
elicitation.url. (#282) - A percent-encoded resource template value reaches the handler decoded. (#282)
- The most specific resource template serves a URI, not whichever was registered first. (#282)
- A path parameter no handler parameter declares is documented in OpenAPI and in the tool schema. (#282)
- JSON that is not a Request object is
-32600, not the-32700reserved for unreadable input. (#282) - A request carrying a null id is refused; MCP requires a string or integer id. (#282)
- An MCP call no longer leaves its request bound, which corrupted the HTTP transport's own request. (#282)
- An abandoned MCP request releases its cancellation-registry entry instead of stranding it. (#282)
- A tool returning a
@dataclassorTypedDictpublishes anoutputSchemaandstructuredContent. (#282) - An optional tool parameter advertises its null branch, and a parameter's default is published. (#282)
- A sub-dependency's body model advertises its fields, so a call built from the schema is accepted. (#282)
- A tool returning
bytesreports the decoded text, or base64 when the bytes are not text. (#282) - A
@dataclassparameter is validated and passed as the dataclass instead of failing on every call. (#282) - A
TypedDictparameter declares an object schema, matching what the handler accepts. (#282) client_host,client_portandremote_addrreport the peer on the ASGI path. (#282)- An authorization failure inside a tool is reported as forbidden, not as an internal error. (#282)
- A modern-revision client's identity and capabilities are read from each request's
_meta. (#282)
[0.15.0] - 2026-08-20¶
Added¶
- MCP serves the
2026-07-28revision alongside the handshake revisions, selected per request. (#276) server/discoveradvertises the served protocol versions, capabilities, and server identity. (#276)- An MCP request naming an unserved protocol version is rejected with
-32022listing what is served. (#276) Query(group=True)reads a model annotation's fields from the query string. (#274)group=Trueis accepted byHeader,Cookie, andFormfor the same field spread. (#274)SessionAuthresolves a cookie session into the request'sPrincipal. (#274)login_sessionandlogout_sessionsign a subject in and out, rotating the session id. (#274)HealthPluginserves/livezand/readyz, failing readiness once shutdown begins. (#274)app.add_lifespan()registers additional lifespan context managers on the app's exit stack. (#274)
Changed¶
Request.content_lengthreads the raw header tuples instead of materializingHeaders. (#275)
Fixed¶
- An
HTTPExceptionreports the same body over MCP and background tasks as it does over HTTP. (#274) - An MCP tool call with invalid arguments is reported in-band as
isError, not as a protocol error. (#277) - An MCP argument-validation message names the offending argument instead of rendering a Python repr. (#277)
[0.14.0] - 2026-08-19¶
Added¶
requestandcsp_nonceresolve in templates without threading them through the render context. (#272)csp_nonce()reads the request being handled when called without one. (#272)
Changed¶
instrument_with_prometheusreports a collector-name collision with theregistry=andprefix=fixes. (#272)
[0.13.0] - 2026-08-19¶
Added¶
ws.appexposes the serving application on a WebSocket, mirroringrequest.app. (#269)RateLimitMiddleware(strict_overrides=False)warns instead of failing on an override key matching no route. (#269)
Changed¶
WebSocketdeclares__slots__, cutting per-connection memory; attach data tows.state. (#270)SessionStoredeclares__slots__, so a slotted store subclass no longer carries a__dict__. (#270)
Fixed¶
- A parameter marker's default is applied when an MCP tool call omits the field. (#269)
- An MCP tool's
inputSchemaadvertises a parameter marker's declared default. (#269) X-RateLimit-Resetnever advertises a wait longer than the configured window. (#269)
[0.12.1] - 2026-08-16¶
Fixed¶
- A scalar
Body()parameter is documented in the OpenAPIrequestBodyinstead of omitted. (#267) Body(embed=True)params document one JSON object body, required only when a field is. (#267)- On Python 3.10, an
Annotated[T, Body()]parameter defaulting toNoneis read from the body, not the query string. (#267)
[0.12.0] - 2026-08-16¶
Changed¶
- A handler's return annotation now supplies
response_model, so the response is filtered to the annotated model. (#263) - Pass
response_model=Noneto keep a model return annotation while declaring no response contract. (#263)
Added¶
veloce checkreports routes with no response schema and routes whoseresponse_modelcontradicts the return annotation. (#263)app.response_contract_audit()returns those findings for a pre-deploy script or test. (#263)- A
list[Model]return annotation documents an array response and filters its elements. (#264) - A union return annotation (
A | B,A | None) documents its alternatives asoneOf. (#264) Veloce(debug=True)logs the response-contract findings at startup. (#264)
Fixed¶
- A
response_modelsubclass instance is re-shaped to the declared model instead of leaking its extra fields. (#263)
[0.11.0] - 2026-08-03¶
Added¶
app.install(plugin)registers an app extension in one call — any object with aninstall(self, app)method. (#253)name, when set on a plugin, records it underapp.extensions. (#253)- The
/docsand/redocpages carry the CSP nonce on every script, style, and stylesheet tag. (#260)
Fixed¶
- A response emits one header line per field name, so two casings of one header no longer both ship. (#260)
RateLimitMiddlewarerejects an override key matching no route at startup, not on every request. (#260)veloce customprints the app's CLI group help instead of crashing when no command is given. (#254)
[0.10.0] - 2026-07-06¶
Added¶
@app.queryregisters a route for the HTTPQUERYmethod (RFC 10008) — safe and idempotent likeGET, with a request body likePOST. (#244)
[0.9.0] - 2026-06-24¶
Added¶
SecuritySchemeis the shared base for authentication schemes, owningauto_errorand the__call__(request)contract. (#236)stream=Trueon a route opts its handler into incremental request-body reading viarequest.stream(), instead of buffering the body first. (#222)MCPErrorand typed subclasses (InvalidParamsError,AuthorizationError, others) let an MCP handler raise a specific JSON-RPC error. (#229)- The MCP HTTP transport rejects an unsupported
MCP-Protocol-Versionheader with400. (#230) ProtocolVersionErrorandOriginNotAllowedErrorsurface MCP transport violations as typed errors. (#230)- The MCP HTTP endpoint answers a
GETwith405 Method Not Allowed. (#230) - The MCP SSE stream sends a priming event on open and a
retryfield before closing. (#230) - The MCP
initializeresult emitsinstructionsfrom the app description or summary. (#230) - The MCP
initializeresult emits aserverInfo.titlefrom the app title. (#230) - MCP tool annotations now carry
openWorldHintand the route summary asannotations.title. (#230) - MCP tool
inputSchemaandoutputSchemadeclare the JSON Schema 2020-12 dialect. (#230) Iconobjects on@app.mcp_tool,@app.mcp_prompt, andmcp_icons=routes surface as a primitive'siconsarray. (#230)- MCP content blocks carry optional
audience/priority/lastModifiedannotations. (#230) ResourceLinkandEmbeddedResourcecontent blocks let a route return a linked or inlined resource result. (#230)@app.mcp_completeranswers MCPcompletion/completewith per-argument value suggestions for a prompt or resource. (#230)- MCP
notifications/cancelledcancels the named in-flight request and unwinds its task. (#230) MCPSessionrecords the client capabilities advertised ininitializeover the stdio transport. (#230)MCP_ENFORCE_LIFECYCLErejects a request that precedesinitializeon a stateful connection. (#230)task_support=Trueopts an MCP tool into a task-augmentedtools/callthat runs in the background. (#230)- A task-augmented
tools/callreturns aCreateTaskResultthe client polls for the result. (#230) tasks/get,tasks/result,tasks/list, andtasks/canceldrive an MCP task through its lifecycle. (#230)- The MCP server emits
notifications/tasks/statuswith the related-task_metaon each task transition. (#230) mount_mcp(transport="http", sessions=True)assigns and validates anMcp-Session-Idon the HTTP transport. (#230)- The MCP HTTP transport rejects a missing required session id with
400and a terminated one with404. (#230) - A
DELETEon the MCP HTTP endpoint terminates the session when session management is enabled. (#230) SessionRequiredErrorandSessionNotFoundErrorsurface MCP session violations as typed errors. (#230)mount_mcp(transport="http", resumable=True)attaches per-stream ids to MCP SSE events and keeps a bounded replay buffer. (#230)- A
GETcarryingLast-Event-IDresumes an MCP SSE stream, replaying only that stream's missed events. (#230) MCP_RESOURCE_SUBSCRIPTIONSlets a clientresources/subscribeandresources/unsubscribeto a resource URI. (#230)MCPServer.notify_resource_updatedsendsnotifications/resources/updatedto subscribed connections. (#230)MCPServer.notify_resources_list_changedsendsnotifications/resources/list_changedto open connections. (#230)MCPContext.sampleasks the client's model for a completion viasampling/createMessage. (#230)MCPContext.elicitrequests user input viaelicitation/createin form or URL mode. (#230)MCPContext.rootslists the client's filesystem roots viaroots/list. (#230)- The stdio transport issues server-to-client requests and awaits their correlated replies. (#230)
MCPCapabilityErrorrejects a server-initiated request the client did not advertise support for. (#230)- The MCP
resourcescapability advertisessubscribeandlistChangedwhen subscriptions are enabled. (#230) - MCP resource subscriptions deliver
notifications/resources/updatedover a stateful HTTPMcp-Session-Idconnection. (#230) - The MCP HTTP transport records the client capabilities from
initializeon a session, gatingMCPContext.sample/elicit/roots. (#230)
Changed¶
- A client disconnecting from an MCP SSE stream no longer cancels the in-flight call. (#230)
MCPContext.cancelledreflects real cancellation state instead of always returningFalse. (#230)- The MCP HTTP transport advertises
resources.subscribe/listChangedastrueonly withsessions=True; a stateless request advertisesfalse. (#230) MCP_ENFORCE_LIFECYCLEis enforced on a stateful HTTPMcp-Session-Idconnection, not only over stdio. (#230)Response.mimetype,charset, andmimetype_paramscache their parse, keyed on the currentcontent_typevalue. (#239)- Route registration rejects a path parameter name that is not a valid Python identifier or is a reserved keyword, instead of failing opaquely at request time. (#240)
Fixed¶
- Response and DI-injected background tasks are tracked and cancelled-and-drained on shutdown, so one no longer outlives the event loop and is orphaned mid-run. (#241)
request.json()caches a JSONnullbody asNoneso it is parsed once instead of re-decoded on every access. (#240)- The MCP HTTP
GETresume path validatesOriginandMCP-Protocol-Versionso a cross-origin or unsupported-version client cannot bypass the DNS-rebinding defense. (#237) - MCP
completion/completebounds the number of client-suppliedcontext.argumentsentries it ingests. (#237) - A malformed inbound
traceparentno longer raises out of the OpenTelemetry span-emit hook; the span is rooted instead. (#237) - An MCP task that settles after a racing
tasks/cancelkeeps itscancelledstatus instead of being overwritten. (#237) - MCP
notifications/cancelledignores a non-scalarrequestIdinstead of raisingTypeErroron the lookup. (#237) PlainTextResponseandHTMLResponsenow acceptbytesas well asstr, matching Starlette parity. (#226)- An MCP HTTP client's
notifications/cancelledcancels only its own in-flight request, never a peer's call with a colliding JSON-RPC id. (#230) - An MCP task is private to the connection that created it;
tasks/listandtasks/get/result/cancelreject another connection's task. (#230) - The MCP HTTP session store evicts idle
Mcp-Session-Idsessions so an abandoned session no longer leaks for the process lifetime. (#230) - The MCP SSE event store caps retained streams so a long-running resumable server's replay buffer no longer grows without bound. (#230)
- An MCP task keys ownership to a stable per-connection id so a task cannot alias to a later session that reuses a freed session's address. (#230)
- Evicting an MCP HTTP session cancels and drops its tasks so a never-settling task no longer pins memory for the process lifetime. (#230)
tasks/canceldelivers itsnotifications/tasks/status(cancelled) reliably instead of dropping it to garbage collection. (#230)- Concurrent MCP SSE streams on one
Mcp-Session-Ideach receive resource-update notifications and unregister independently. (#230) mount_mcp(transport="http")rejects atask_supporttool withoutsessions=Trueso a created task is never silently unretrievable. (#230)- An MCP task runner refuses
ctx.sample/elicit/rootson stdio, settling the task failed instead of racing the serve loop's reader. (#230)
[0.8.0] - 2026-06-13¶
Added¶
Veloce.run(reload=True)andveloce run --reloadauto-restart the built-in server on source changes, without uvicorn. (#212)EVENT_LOOP_WATCHDOGnames the route and dependency a blocking call stalled in. (#210)
[0.7.0] - 2026-06-12¶
Changed¶
- OpenAPI parameters derive from the handler plan the resolver runs, keeping documented and enforced contracts in lockstep. (#205)
clickis now an optionalcliextra; installveloceframework[cli]to useapp.cliandtest_cli_runner. (#206)
Fixed¶
- Parameters the resolver treats as optional are documented as
required: false, matching runtime. (#205) - A form request body whose every field is optional is documented as not required, matching runtime. (#205)
FileResponse.from_pathemits a bareContent-Dispositionfor a non-default disposition with no filename, matching the sync constructor. (#207)
[0.6.0] - 2026-06-10¶
Added¶
veloce new NAME [--template minimal|api|web]scaffolds a project, andveloce generate KIND NAME(aliasg) emits a single file. (#197)get_flashed_messagesis auto-injected as a Jinja global, so templates call it without manual registration. (#197)SessionMiddleware/ServerSessionMiddlewareresolve unset constructor arguments fromapp.configon the first request. (#198)app.secret_keyis a live property bound toconfig["SECRET_KEY"], so it alone configuresSessionMiddleware. (#198)send_file/async_send_fileapplySEND_FILE_MAX_AGE_DEFAULTwhen called withoutmax_age=. (#198)
Security¶
CORSMiddleware(allow_origin_regex=...)gates strictly by the regex instead of defaultingallow_originsto["*"]. (#197)
Changed¶
APIRouternow aliasesRouter(wasBlueprint); constructBlueprintfor a named route group. (#198)MAX_CONTENT_LENGTHdefaults to104857600(100 MiB); set it toNonefor unlimited. (#198)- A failing
yield-dependency teardown now reachesgot_request_exceptionand re-raises underPROPAGATE_EXCEPTIONS. (#198)
Fixed¶
/docsrenders withBaseLayoutinstead of the unloadedStandaloneLayout. (#197)@rate_limitis honored oninclude_in_schema=Falseroutes (with the strategy API). (#197)@app.endpoint(name)reclassifies the route so a sync view is offloaded, not awaited. (#198)PROPAGATE_EXCEPTIONS=false(and0/off) from an env file now reads as off. (#198)security_audit()no longer claims session signing falls back to weak defaults whenSECRET_KEYis unset. (#198)- The native server drops chunked-request trailer fields instead of prepending them to the next request. (#198)
- A mounted sub-app's trailing-slash redirect carries the mount prefix in its
Location. (#197) - A non-ASCII
query_stringover ASGI returns400instead of raising a500. (#197) - A
multipart/form-databody that fails mid-parse returns400, not a partial200. (#197) StreamingResponseon the native server no longer truncates on an emptybyteschunk. (#197)- Registering
/usersand/users/no longer flips the first to a slash redirect. (#197) - Blueprint routes keep
exclude_middleware=[...]afterregister_blueprint. (#197) - A mutable parameter default (
tags: list[str] = []) is no longer shared across requests. (#197) ProxyFixkeeps the brackets and port of aForwardedIPv6host. (#197)- A native-server
HEADresponse no longer sends a body, keepingContent-Length. (#197) - The native server no longer drops a WebSocket frame pipelined into the handshake segment. (#197)
- A non-WebSocket
Upgrade(e.g.h2c) returns400without running the route handler. (#197)
[0.5.0] - 2026-06-10¶
Added¶
- MCP HTTP transport hardening:
mount_mcp(transport="http", allowed_origins=[...])validates theOriginheader (DNS-rebinding defense), andexclude_middleware=[...]drops named app middleware from the/mcp+ metadata routes (so an app-wide auth middleware the transport's ownauthreplaces does not run on it). (#194) - MCP authorization:
mount_mcp(transport="http", auth=MCPAuth(...))makes the endpoint an OAuth 2.1 resource server — a user-suppliedverifycallable validates the bearer token on every request, the RFC 9728 protected-resource metadata is served, and a missing/invalid token returns401(insufficient endpoint scope returns403) with aWWW-Authenticatechallenge. Declarative per-tool scopes (@app.mcp_tool(scopes=...),mcp_scopes=on exposed routes) are enforced against the request principal. (#194) Principal+current_principal()/set_principal(): a unified authenticated identity populated by HTTP auth or the MCP transport, so authorization and identity-aware dependencies read one source across both doors. (#194)Request.is_mcpmarks a replayed MCP tool/resource call, so auth middleware can defer to the transport on agent calls while business middleware runs unchanged. (#194)- MCP Streamable HTTP transport:
app.mount_mcp(transport="http", path="/mcp")mounts the MCP server as aPOSTroute, so it can run as a remote/hosted server under any ASGI server. A request withAccept: text/event-streamis answered with an SSE stream of the call's progress/log notifications followed by the JSON-RPC response; otherwise a single JSON response. The route is protected by whatever middleware and dependencies the app applies to it. (#194) - MCP progress and logging:
MCPContext.report_progress(...)andMCPContext.log(...)now send livenotifications/progressandnotifications/messageto the client (progress requires the client'sprogressToken); the server handleslogging/setLeveland advertises theloggingcapability. (#194) - MCP per-call timeout: set
app.config["MCP_CALL_TIMEOUT"](seconds) to bound each tool call, resource read, and prompt render; an overrun is cancelled and surfaced as an in-band tool error or a JSON-RPC error. Unset (no timeout) by default. (#194) - MCP prompts: register a reusable prompt template with
@app.mcp_prompt(...). The callable's parameters become the prompt's arguments and its return (a string or a list of role/content messages) becomes the rendered messages; the server answersprompts/listandprompts/get, withDepends/MCPContextresolved as in a tool, and advertises thepromptscapability when at least one is registered. (#194) - MCP resources: expose a read-only (
GET/HEAD) route as a Model Context Protocol resource withexpose_as_mcp_resource=Trueandmcp_resource_uri=...(a static URI, or a URI template such asusers://{user_id}binding the route's path parameters). The server answersresources/list,resources/templates/list, andresources/read, replaying the route's dependencies, security, andresponse_modelthrough the shared invocation path; it advertises theresourcescapability when at least one resource is registered. (#194) - MCP non-text tool content: a tool returning an
image/*oraudio/*response emits the matching typed MCP content block (base64), and a binary resource read returns its bytes as ablob. (#194)
Fixed¶
- The native dev server (
app.run()) now starts on Windows:reuse_portis requested only whereSO_REUSEPORTexists, instead of unconditionally passingreuse_port=Trueto the selector event loop (which raisedValueErrorand killed the serving thread before it bound). (#195) - The native dev server now drains in-flight requests on shutdown on Windows too:
where
loop.add_signal_handleris unavailable,_servefalls back tosignal.signaland schedules the cooperative shutdown on the loop, so Ctrl+C / Ctrl+Break let an in-flight request finish at its boundary instead of raisingKeyboardInterruptstraight out of the loop and resetting the connection. (#195) - Blueprint error handlers are now scoped to their own routes: a
@bp.errorhandleronly catches exceptions raised on that blueprint (or a nested descendant), consulted by the failing request's blueprint chain before the app-level handlers — it no longer catches a sibling blueprint's or an app-level route's exception.error_handler_specnow reports per-blueprint sub-tables. (#195) - A mounted Veloce sub-app now sees
request.root_path(andscript_root) set to its mount prefix, matching mounted ASGI apps, sourl_forand proxy-aware URLs inside the sub-app are prefix-correct. (#195) JSONResponse,HTMLResponse, andPlainTextResponseacceptbackground=(forwarded to the baseResponse), so aBackgroundTask/BackgroundTaskscan be attached to them as it can toResponse. (#195)FileResponse(content_disposition_type="inline")now emitsContent-Disposition: inlineeven without afilename; an explicit non-default disposition is honoured (the defaultattachmentwithout a filename still emits no header, so plain file responses are not forced to download). (#195)- The
sessionproxy forwards attribute writes, sosession.permanent = Trueworks through the global proxy rather than raisingAttributeError. (#195) - A single Pydantic body model's validation errors are now located under
"body"(e.g.["body", "field"]), consistent withBody(...)marker params and the whole-body error cases. (#195) - MCP: the
logging/setLevelminimum is now scoped per request (a ContextVar like the progress/notification channel) rather than on the sharedMCPServer, so one HTTP client's level change no longer raises the notification floor for others. (#194) - MCP: a resource read short-circuited by an auth guard (
401/403) maps to a forbidden error rather than an internal error. (#194)
Security¶
- MCP: a pure
@app.mcp_toolhandler error (and the defensive internal-error path) surfaces a generic message unlessapp.debugis set, so an exception carrying a secret is not returned verbatim to the agent. (#194) - MCP: a tool argument can no longer masquerade as an
Authorization/Cookieheader on the replayed request, so aSecurityscheme cannot read agent-supplied input as a credential;Principal.tokenis excluded fromrepr();MCPAuthrequiresresource_server_url+authorization_servers; and an insufficient scope is reported uniformly across tools/resources/prompts (HTTP 403 with aWWW-Authenticatechallenge over the JSON transport). (#194)
[0.4.0] - 2026-06-08¶
Added¶
- Configurable rate limiting: selectable algorithms (
FixedWindow,SlidingWindow,TokenBucket), pluggable in-memory or Redis backends, and per-route limits viaoverridesor the@rate_limitdecorator. - Result caching: the
cacheddecorator withInMemoryCacheandRedisCache. (#171) veloce.contrib.redis:RedisSessionStore,RedisRateLimitBackend, andRedisCachefor state shared across workers.- msgspec as an opt-in fast validation and serialization backend. (#157)
- Model Context Protocol integration (
veloce.contrib.mcp): tool exposure over stdio, protocol-version negotiation,ping, route-derived tool metadata, and streaming-result tools. - JSON Web Tokens (
encode_jwt/decode_jwt), storage-free reset tokens (make_reset_token/check_reset_token), and aSecretwrapper that resists accidental disclosure. (#139) CSPMiddleware(Content-Security-Policy with a per-request nonce and report-only mode) andConditionalGetMiddleware(304forIf-None-Match/If-Modified-Since). (#139)CORSMiddlewaregains Private Network Access support and preflight-method validation;CSRFMiddlewaregains Origin verification viatrusted_origins. (#136)- Middleware ordering with
add_middleware(..., priority=N)and per-route opt-out withexclude_middleware=[...]. - Background-task supervision:
app.supervise(...)(restart policy) andapp.spawn(...)(app-scoped tasks). - Routing: constrained converter syntax (
{x:converter(arg)}),date/time/ decimal path converters, duplicate-route detection, and the declarative@app.websocket_listenerroute. StaticFiles: precompressed-sibling serving,html=Truedirectory indexes, and write-sideIf-Match/If-Unmodified-Sincepreconditions.- WebSockets: native-transport server support on
Veloce.run(), an idle-receive timeout, async-context-manager support, heartbeats, send backpressure, and UTF-8 / close-frame validation. - Server-Sent Events:
ServerSentEvent.commentand.json, bare-value source iterators, and a proactive heartbeat. - Observability:
instrument_access_log/log_requests_as_json, a Prometheus exporter (instrument_with_prometheus), and an OpenTelemetry bridge with a live-tracing mode and anon_spanhook. - OpenAPI: separate request/response schemas, identity-keyed components,
operationId de-duplication, a documented
422response, and avalidate_openapiflag. - Sessions: sliding expiry,
domain=/ chunked-cookie options, andVary: Cookieon cookie-varying responses. - Encoder extensibility: a per-call
custom_encoder, process-levelregister_encoder, and broader built-in coverage (bytes,set/frozenset,pathlib.Path,re.Pattern, scalar subclasses). - Deployment: optional gunicorn
VeloceWorker, built-in dev-server TLS, an ASGI-appmount,.envloading, a dev event-loop watchdog, and an asyncTestClient.uvicornis now an optional extra rather than a hard dependency. - New top-level exports:
Config,Aborter,URLRule,SetupError,JSONProvider/DefaultJSONProvider/config_orjson_options,get_openapi_schema/setup_openapi_routes,StaticFiles,Jinja2Templates,log_requests_as_json, andasync_send_file. - Developer documentation: a build-one-app tutorial, a runnable
examples/directory, a databases guide, and a Hypothesis fuzzing harness across the parsers, router, signing, and WebSocket paths.
Changed¶
- The deprecated
Veloce.on_event()/Veloce.add_event_handler()now target removal in1.0.0. (#173) Veloce.run(workers=...)raisesValueErrorfor any worker count other than1(the built-in server is single-process). (#166)- Independent dependencies resolve concurrently, and a no-wave
Dependschain compiles to a straight-line async resolver. (#154) - Numerous per-request and schema-generation paths were optimized — a compiled feature pipeline, indexed route/encoder lookups, and bounded caches — without changing public behavior.
- Route resolution gates its mounted-app, static-handler, and ASGI-mount scans on the compiled pipeline flags, skipping each scan when nothing of that kind is registered. (#183)
- Literal request paths resolve through a registration-time exact-match map in one
hash lookup instead of a radix-tree walk, falling through to the tree for
parameterized, wildcard, and slash-redirect routes (literal
match()~1.7x faster, ~3x on deep literal paths). (#185) - Requests to feature-free apps take a straight-line dispatch fast path: when no
middleware, request/response hooks, mounts, or url-value preprocessors are
registered and the matched route is an async trivial or request-only handler
with no response model, custom response class, non-default status, host or
subdomain constraint, defaults, or middleware exclusion, the middleware, hook,
route-resolution, and dependency-resolution orchestration is skipped while
coercion,
after_this_requestcallbacks, background tasks, exception handling, and teardown remain shared (~6-8% lower per-request dispatch time on those routes, in-process A/B). (#185)
Fixed¶
- Per-route rate-limit state now rebuilds when routes are added after startup. (#178)
- A bodiless status (
1xx,204,205,304) no longer advertises a body, the WebSocket handshake uses the correct RFC 6455 GUID, and a frame with a non-zero RSV bit is rejected. HTTPBasic/HTTPDigestescape therealm, non-latin-1 header values are RFC 2047 encoded, anddecode_jwtrejects an empty secret.- JSON serialization handles
set/frozenset,pathlib.Path, integer-valuedDecimal, andexclude_none;StaticFilesprecompressed selection returns406and honours an explicitq=0. - Assorted correctness fixes across OpenAPI dual-schema comparison, scope-aware
dependency caching,
instrument_with_otelidempotency, signal delivery, and per-route middleware-exclusion symmetry.
Security¶
LoggingMiddlewareand the access log escape control characters in request-derived fields (CWE-117 log forging), andRequestIDMiddlewaresanitizes an inbound request id.- Security headers are matched case-insensitively so a handler override is not
silently replaced; the cookie writer round-trips a literal
%. - The native WebSocket server rejects an unmasked client frame, HTTP Basic
rejects an RFC 7617-malformed credential, and
dump_cookierejects a non-token cookie name. safe_joinrejects Windows reserved device names,URL.from_requestvalidates theHostheader (RFC 3986), and the router rejects a path that binds one parameter name twice.
[0.3.0] - 2026-06-01¶
Fixed¶
StaticFilesnow applies RFC 9110If-Rangevalidation correctly before serving partial responses. (#128)
[0.2.0] - 2026-05-31¶
Added¶
- Streaming request bodies on the built-in HTTP server, so large uploads no longer require buffering the full body before dispatch. (#106)
- CLI plugin discovery,
.envloading, template streaming, SSE heartbeat support, OpenTelemetry integration, and a signal namespace helper. - Hybrid routing for patterns that do not fit the radix tree, plus an optional gunicorn worker.
- Broader documentation coverage across configuration, templates, static files, sessions, signals, and related framework guides.
Changed¶
- Request body access is now asynchronous:
request.body(),request.text(), andrequest.get_data()must be awaited. (#106) request.stream()now streams on the raw HTTP path instead of replaying an already-buffered body. (#106)- Debug mode renders an HTML traceback page for clients that prefer HTML while preserving plain-text tracebacks for CLI and programmatic clients. (#117)
- Resolver and response-encoding internals were consolidated and optimized without changing the public API.
Fixed¶
- Correct handling for
If-Range, partial-content gzip behavior, async template context processors, duplicate response headers, hybrid-router edge cases, and gunicorn worker lifecycle/TLS behavior.
Security¶
- Restored strict header validation on streamed responses.
- Applied the same form-field limits to URL-encoded bodies as multipart forms.
[0.1.4] - 2026-05-25¶
Changed¶
- Focused maintenance release covering security hardening, correctness fixes, API cleanup, and small internal consolidations.
- Improved encoder behavior, cached more parsed request metadata, and reduced duplicated logic across middleware, CLI helpers, templating, and the test client. (#95)
Security¶
- Tightened multipart UTF-8 validation,
HTTPBasicchallenge construction, and exception handling around basic-auth parsing. (#95) - Made HSTS subdomain coverage opt-in rather than implicit. (#95)
Removed¶
- Dropped unused internal constants from the handler-plan implementation. (#95)
[0.1.3] - 2026-05-23¶
Changed¶
- Security and correctness release covering CSRF token rotation, password-hash parameter validation, and several framework/runtime fixes.
- Improved diagnostics around OpenAPI schema generation and clarified the process-local scope of the built-in rate limiter. (#94)
Fixed¶
- Addressed loop-affinity issues in
Veloce(), multipart encoding in the test client, stale response-encode caches, router merge behavior, and several runtime guards that previously relied onassert. (#94)
Security¶
- Added CSRF token rotation support after login or privilege changes. (#94)
- Rejected weak or tampered scrypt parameters during password verification. (#94)
- Added SRI protection for Swagger UI and ReDoc assets. (#94)
[0.1.2] - 2026-05-23¶
Added¶
- Top-level exports for
render_template,render_template_string, andJinja2Templates. (#78)
Changed¶
Request.json()became asynchronous for consistency with the rest of the request-body API. (#78)- Runtime dependencies were corrected so standard installs include the pieces needed for documented framework features. (#78)
veloce.__version__now comes from installed package metadata. (#78)
[0.1.1] - 2026-05-23¶
Changed¶
- Metadata-only release correcting maintainer information in the published package.
[0.1.0] - 2026-05-23¶
Added¶
- Initial public release of Veloce as
veloceframework. - Core framework surface including the
Veloceapp, radix-tree routing, request/response primitives, dependency injection, OpenAPI generation, and an in-memoryTestClient. - Built-in middleware, sessions, templating, signals, background tasks, Server-Sent Events, WebSockets, security helpers, and class-based views.
- CLI commands, static-file support, instrumentation hooks, server-side sessions, async password helpers, and the first round of performance-focused hot-path improvements.
Changed¶
- Set safer defaults and improved consistency across response handling, multipart uploads, WebSocket dependency injection, and request streaming.
Fixed¶
- Corrected early issues in blueprint registration, SSE encoding, dependency coercion, multipart cleanup, session-store race handling, static-file caching, logging, and request-scoped resource cleanup.
Security¶
- Added incremental request-size enforcement, request timeouts, WebSocket origin checks, security headers, signed CSRF tokens, multipart limits, and secure deployment audit helpers.