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]¶
[0.1.2] - 2026-05-23¶
First post-release iteration. Closes four rough edges surfaced by the
end-to-end smoke test of the published veloceframework==0.1.1 wheel.
Changed¶
Request.json()is nowasync(#74). Previous releases shippedjson()as a synchronous method whileform()was alreadyasync; the asymmetry broke theawait request.json()idiom Starlette, FastAPI, and Quart callers reach for first. Migration: any call site that wroterequest.json()now writesawait request.json(). The Flask-flavouredrequest.get_json()stays synchronous so Flask muscle-memory continues to work.pyproject.tomlruntime dependencies extended withuvicorn[standard],jinja2, andclick(#77). They were declared only in the dev dependency group on 0.1.1, so a freshpip install veloceframeworkleft users withoutuvicornon the path and withoutjinja2for the templating helpers the docs point at. WebSocket support stays opt-in through the newveloceframework[ws]extra (pip install veloceframework[ws]) so REST-only deploys do not pull thewebsocketslibrary.veloce.__version__is now derived from package metadata viaimportlib.metadata.version("veloceframework")(#75), with a literal fallback for editable installs without materialised metadata. The hand-maintained constant in__init__.pycould and did drift from the wheel'spyproject.tomlversion (0.1.0vs0.1.1on the previous release); deriving from metadata makes the two impossible to disagree.
Added¶
render_template,render_template_string, andJinja2Templatesare now exported from the top-levelvelocepackage (#76). The helpers always lived underveloce.contrib.templating; surfacing them at the root matches the Flask muscle-memory that the rest of the Veloce API preserves (g,flash,current_app,before_request,redirect,make_response,abort,url_for).
[0.1.1] - 2026-05-23¶
Metadata-only release. No code, behaviour, or dependency changes against 0.1.0 — this version exists solely to correct the maintainer email recorded in the PyPI package metadata.
Changed¶
pyproject.toml:authorsandmaintainersemail corrected fromrevanthravella@gmail.comtolokeshtallapaneni@gmail.com. The PyPI v0.1.0 metadata is immutable, so the fix lands as 0.1.1; users on v0.1.0 will pick up the corrected metadata on the nextpip install --upgrade veloceframework.
[0.1.0] - 2026-05-23¶
First public release. Veloce is published to PyPI as veloceframework;
the import name veloce is unchanged.
Highlights¶
- Async-first ASGI core with a hand-written radix-tree router, custom
request/response pipeline, in-memory
TestClient, and a dependency injection system that resolves precompiled plans (HandlerPlan) at registration time so the per-request hot path performs no reflection. - Feature surface covers Flask 3.x and FastAPI parity for the workflows most apps reach for first — blueprints, dependency injection, OpenAPI generation, Jinja templating, WebSockets, sessions, signals, and a complete Werkzeug-shape request/response API.
- Performance contract: comparative benches in
benchmark.pyshow 3-5x throughput vs equivalent FastAPI handlers and 4-7x vs Flask on the JSON-hello and path-param hot paths.
Added¶
The entries below were authored during the [Unreleased] window and
ship as part of this release.
Changed¶
- Per-request dispatch +21-39 % (profile-driven DSA pass). Walked the
json-hello / path-param hot path under
cProfileand applied seven targeted shaves, each attributed to a measured delta: Veloce._dispatch_requestdefersDependencyResolver()until a non-trivial route demands it. Trivial-plan routes (no injected params, no dependencies) never construct the resolver — saves the resolver allocation + two attribute writes per static-GET request.DependencyResolver.__init__no longer allocates a throwawaydict+WeakKeyDictionaryfor_overrides/_override_subplans; they default to module-level empty sentinels and the dispatcher swaps in the real instances only when overrides exist.Request.headersis now a lazy property backed by_headers_raw(raw ASGI(bytes, bytes)tuples). TheCIMultiDict+ per-tuplelatin-1decode is built only on first read. The hot path never readsrequest.headers, so 2-3 us / req of work was being burned on every dispatch._run_response_middlewareis gated at the main hot-path return so the no-op coroutine + await is skipped when no middleware is registered (avoids ~940 ns / req of frame setup)._asgi_appreadsscope["path"]/scope["query_string"]via subscript (ASGI mandates both keys), skippingdict.getdefault handling.- Built-in
Content-Typestrings (application/json,text/html; charset=utf-8,text/plain; charset=utf-8,application/octet-stream) and smallContent-Lengthvalues (0-2047) hit precomputed bytes caches; the per-request_reject_header_crlf(...).encode()+str(n).encode()allocations are skipped on cache hit. Response._streamjoins__slots__initialised toNone; theis_streamed/freeze/iter_encoded/iter_chunked/cache_controllookups become a direct slot load instead of agetattr(..., None)walk. In-loop bench (bench/hot_dispatch_bench.py) median of 3 runs: static GET 68.1k → 94.8k req/s (+39 %), path-param GET 54.5k → 66.0k (+21 %), POST 64-byte body 62.0k → 77.2k (+25 %). cProfile total time for 16k mixed dispatches dropped 1.51 s → 0.59 s (~2.56×).- Stdlib
jsondropped in favour oforjsonat the remaining two sites.Config.from_prefixed_env's defaultloadsis noworjson.loads;Config.from_file's defaultloadis a new tiny_orjson_load(fp)adaptor (orjson has no file-object loader).Swagger UIHTML render emitsswagger_ui_parametersandswagger_ui_init_oauthviaorjson.dumps(...).decode(). orjson produces compact JSON (no space after:); the on-wire format for embedded literals is now"key":valuerather than"key": value— the whitespace was never part of any contract and the JS parser consumes both identically. Behaviour-equivalent for valid JSON; catch sites unchanged becauseorjson.JSONDecodeErroris aValueErrorsubclass. - Per-request dispatch ~+17 %. Profile-driven pass over the in-loop
ASGI hot path:
_setup_openapigated at call sites so the no-op branch costs one attribute read instead of a frame;_endpoint_blueprintno longer parsed three times per request when no blueprint hooks are registered;Headers, thecurrent_app/current_requestcontextvars, and therequest_started/request_finishedsignals hoisted to module top instead of being re-imported per request; single-chunk request body fast-path skips thebody_partslist +b"".join;_reject_header_crlfinlined as three short-circuitedinchecks;Signal.has_receivers_forshort-circuits on empty subscriber list;_run_teardownsawait skipped when no yield-dependencies registered. In-loop bench (bench/hot_dispatch_bench.py): static GET 62.5k → 72.8k req/s (+16.5 %), path-param 47.8k → 57.0k (+19.4 %), POST 64-byte body 54.8k → 64.2k (+17.2 %). - Router micro-ops.
Router.matchtries the raw method onhandlers.getbeforemethod.upper()(RFC-conforming clients send uppercase already);_match_nodeflattens static-only descent into awhileloop when the current node has no param/wildcard alternatives, shaving one Python frame per static segment; single-param-child path skips the rollbackdel(no alternative to back off to);FloatConverter.matchchecks"e" / "E"directly instead of allocatingvalue.lower(). - Per-request rate-limit O(N) → O(1).
RateLimitMiddlewareswitches from per-request list comprehension tocollections.deque+ amortisedpopleft. Periodic eviction sweep now mutates in place with a snapshot-then-recheck guard so an append racing with the sweep is not silently dropped. - Override-dependency sub-plan cache hoisted to the app. Each
request's fresh
DependencyResolversharesVeloce._override_subplans, eliminating the per-requestbuild_plan - triple
inspect.is*functionprobe on override hits. The cache is cleared whendependency_overridesis reassigned. - Mount-prefix slash precomputed.
_mounted_apps/_asgi_mountsnow store(prefix, prefix + "/", app)so dispatch doesn't reallocateprefix + "/"per request per mount. - Exception-handler signature cache.
_call_exc_handlermemoises(wants_request, wants_exc)flags per handler in aWeakKeyDictionary, eliminating theinspect.signaturewalk per raised exception. jsonable_encoderprimitives short-circuit. TheNone | str | int | float | boolbranch is hoisted to the top of the dispatch so leaf calls hit it before any of the heavierisinstancechecks.
Fixed¶
- ETag drift between StaticFiles and FileResponse.
StaticFiles._compute_etagnow delegates toveloce.http.response._file_etagso a static handler andFileResponseover the same file emit identical ETags and validate identically againstIf-None-Match. Signature changed from(path, mtime)to(path, size, mtime)—_-prefixed and therefore private, but flagged for subclassers. - WebSocket ASGI-mode unbounded queue.
WebSocket.from_asgibuilds_receive_queuewithmaxsize=DEFAULT_RECV_QUEUE_MAXSIZEinstead of an unbounded queue. The queue is unused in ASGI mode today; the bound prevents a footgun if future changes start feeding it.
Added¶
- Comparative bench harness (
bench/comparative/): head-to-head latency and throughput measurements vs Flask and FastAPI under the same uvicorn runtime. Each workload runs all three frameworks in randomised order through a singlehttpx.AsyncClient, with a discarded cold-cache round to dampen first-run penalties. Reports median rps, p50, p99.--seedpins the schedule for reproducibility. Initial workloads:json-helloandpath-param. Results recorded underdocs/bench/. Veloce wins rps + p50 + p99 vs FastAPI on both workloads, and wins rps vs Flask by ~57 % (Flask wins p50/p99 underasgiref.WsgiToAsgiat low concurrency — see caveats indocs/bench/README.md). WebSocket.originaccessor returns the handshakeOriginheader (orNone);WebSocket.check_origin(allowed)returnsTrueonly when the origin is on the allow-list. Normalisation (.rstrip("/").lower()) and wildcard ("*") semantics match the registered-onceWebSocketOriginMiddleware, so allow-lists are interchangeable between the two APIs.Origin: null(sandboxed iframes /file://) is rejected. The pair lets handlers reject Cross-Site WebSocket Hijacking beforeaccept()— the WebSocket handshake is plain HTTP, so Same-Origin Policy and CORS do not apply.- Application core —
Veloceapp object with HTTP method decorators (get/post/put/patch/delete/head/options/trace), lifespan handling, configurable docs URLs, andapp.run(). - Radix-tree router — typed path converters (
int,float,str,uuid,path, custom registered converters), per-routestrict_slashes, subdomain and host constraints, and rule defaults. - Request / Response — lazily-parsed
Requestwith multi-value query/header/cookie/form accessors, parsed conditional and range headers, and aResponsefamily (JSONResponse,HTMLResponse,PlainTextResponse,RedirectResponse,StreamingResponse,FileResponse,ORJSONResponse,UJSONResponse). - Dependency injection —
Depends/Security/SecurityScopes,yield-style dependencies with teardown,Annotated[...]form, bareDepends()annotation inference, andapp.dependency_overrides. - Parameters —
Query,Path,Header,Cookie,Body,Form,Filemarkers with constraints (ge/le/gt/lt,min_length/max_length,pattern,multiple_of), list-valued collection, andinclude_in_schema/title/examples. - OpenAPI 3.1 — auto-generated schema, Swagger UI and ReDoc, security
scheme emission, route-level
responses/callbacks/openapi_extra, and a documentation-onlyapp.webhooksrouter. - WebSockets — ASGI WebSocket routing, raw message API, JSON/text/
bytes iteration, subprotocol negotiation, dependency injection, and
WebSocketException/WebSocketRequestValidationError. - Middleware — CORS, GZip, TrustedHost, HTTPS redirect, sessions, rate limiting, request ID, proxy header handling, plus a dispatch-style base class.
- Templating — Jinja2 integration with async rendering, context processors, and registered filters/globals/tests.
- Sessions — signed, timestamped cookie sessions with secret
rotation, a mutation-tracking
Sessioncontainer, and persistent (permanent) sessions. - Utilities — background tasks, signals, Server-Sent Events,
blueprints with nesting, class-based views, a CLI (
veloce run/veloce routes/veloce shell), and an in-memoryTestClient. - Security helpers — HTTP Basic / Bearer / Digest, API key schemes, OAuth2 password and authorization-code flows, password hashing, and signed-value serialisation.
veloce.statusgainsHTTP_208_ALREADY_REPORTED,HTTP_226_IM_USED, andHTTP_421_MISDIRECTED_REQUESTfor full IANA HTTP status coverage.constant_time_compare(a, b)— a timing-safe secret-comparison helper (wrappinghmac.compare_digest), exported from the top-level package.- Veloce's
WebSocketframe parser reassembles fragmented messages — aFIN=0data frame followed by continuation frames (RFC 6455 §5.4); control frames may be interleaved without disturbing the in-progress message. add_middlewarenow accepts a standard ASGI middleware class — any class that is not a veloceMiddlewaresubclass is treated as ASGI middleware and wraps the whole application (cls(app, **options)), so the third-party ASGI ecosystem (tracing, profiling, observability) plugs into a veloce app. The first-registered ASGI middleware is the outermost wrapper. NativeMiddlewareclasses are unaffected.AsyncTestClient(and theapp.async_test_client()factory) — the async counterpart ofTestClient. Used asasync withinside an async test, its request methods are coroutines awaited on the test's own running event loop. Cookie persistence, redirect following, and the JSON / form / files body shapes matchTestClient.app.mount(prefix, app)now accepts any ASGI application, not only a veloce sub-app. A non-veloce app is dispatched at the ASGI layer with the matched prefix moved from the scope'spathontoroot_path; veloce sub-apps keep their existing dispatch path. A mounted ASGI app receiveshttpandwebsocketscopes — the parent app owns thelifespancycle, so a mounted app must self-initialise rather than rely on ASGIlifespanevents. Mount prefixes must not overlap.Config.from_env_file(path)loads a dotenv-style.envfile —KEY=VALUElines,#comments, an optionalexportprefix, and quoted values — into the app config (UPPERCASE keys only).app.run(ssl_context=...)— the built-in development server now accepts an optionalssl.SSLContext, handed straight toloop.create_server(ssl=...), for local HTTPS testing. Left unset the serving path is byte-for-byte the same plain-HTTP path as before. Production should still terminate TLS at uvicorn or a reverse proxy.EventLoopWatchdog— an opt-in development aid that detects a coroutine blocking the event loop (a synchronous driver,time.sleep, a CPU-heavy loop) and logs a warning carrying the blocked stack and a prescriptive hint (blocking-I/O vs CPU-bound). A loop heartbeat plus a separate daemon thread spot the stall. Enable it with theEVENT_LOOP_WATCHDOGconfig key; unset (the default) nothing is constructed, so a production app pays nothing.ServerSessionMiddlewarekeeps the session payload server-side in a pluggableSessionStore(default: an in-processInMemorySessionStore) — the cookie carries only an opaque, high-entropy session id. Sessions are now revocable: empty one in a handler (session.clear()) or delete it straight from the store (await store.delete(session_id)), and a tampered or stale cookie simply fails to resolve. A network backend (e.g. Redis) plugs in by implementing the asyncSessionStoreinterface. The existing signed-cookieSessionMiddlewareis unchanged.app.add_instrumentation(hook)registers an observability hook called once per finished HTTP request with aRequestMetricsrecord — method, concrete path, matched route template (a low-cardinality metric label), status code, and wall-clock duration. Hooks may be sync or async; one that raises is logged and never breaks the response. With no hook registered the request path pays nothing — not even a clock read. Therequest_started/request_finishedsignals now also carry theRequest, so a tracing bridge can correlate a request's start with its finish.- Query / path / header / cookie parameters are now validated through
Pydantic for any annotation the fast scalar path does not cover —
datetime,date,time,UUID,Decimal,Literal[...]and other rich types are parsed and rejected with a422on bad input, the same treatment a request-body model already received. Thestr/int/float/bool/Enumdispatch fast path is untouched. OpenAPI parameter schemas now emit the matchingformat/enumkeywords instead of collapsing every non-primitive to a bare string.
Changed¶
- A handler that returns a bare
strnow defaults toContent-Type: text/html; charset=utf-8(previouslytext/plain), so a bare-strreturn andmake_response(str)produce the same media type. - Multipart form parsing now uses the
python-multipartstreaming parser instead of an in-memorybody.split— it correctly handles a boundary token that happens to occur inside binary file data, and a malformed body degrades to the parts that parsed cleanly rather than a500. app.run()starts the built-in development server; it now logs a startup reminder that production deployments should run under uvicorn (or another ASGI server). See the new Deployment guide.Response.set_cookienow defaultssamesiteto"Lax"— a CSRF-resistant default that matches modern browser behaviour. Passsamesite=Noneto omit the attribute, or"None"(withsecure=True) for a genuinely cross-site cookie.- WebSocket dependency injection now runs through the same pre-planned
HandlerPlan/DependencyResolveras HTTP dispatch. WebSocket dependencies gainyield-style teardown andSecurity/SecurityScopessupport, and path parameters are coerced to their annotated type — previously WebSocket DI used a separate, weaker resolver that supported none of these. - An uploaded file is now backed by a
SpooledTemporaryFile: each multipart part streams into one as it is parsed, staying in memory while small and rolling over to a real temp file on disk once it grows past 1 MiB. A large upload no longer holds two or three full copies of itself in RAM (raw body + per-partbytearray+BytesIO). request.stream()now yields the body in bounded 64 KiB chunks instead of one chunk covering the whole body, so a handler can process a large body incrementally.
Fixed¶
register_blueprintno longer drops a blueprint's routes registered withinclude_in_schema=False, nor its WebSocket routes — every route is added to the radix tree.EventSourceResponseencodes yieldedServerSentEventobjects over the ASGI transport instead of raisingTypeError.- Dependency type hints are resolved from the right object for class
dependencies (
__init__), callable instances (__call__), andfunctools.partialwrappers, so their parameter types are coerced. - The in-memory
TestClientaccepts WebSocket connect paths that include a query string. WebSocket.send()— the raw ASGI-message escape hatch — now enforces the same handshake state machine assend_text/send_bytes: sending beforeaccept()or afterclose()raises instead of proceeding silently.WebSocket.receive_text/receive_bytes/receive_jsonenforce the same handshake state machine as theirsend_*siblings: calling them beforeaccept()raisesRuntimeError(was: hung on an empty queue) and calling them afterclose()raisesWebSocketDisconnect.- Multipart parsing no longer leaks a
SpooledTemporaryFilewhen a request is rejected by a DoS cap (oversized part or too many parts): the in-progress part's spool and every spool already collected from completed parts are closed on the reject path. - Server-side sessions use a conditional store write: a session revoked by
a concurrent request (logout,
store.delete(...)) while another request is in flight is no longer resurrected when that request writes back —SessionStoregains a race-safereplace()method. - HTTP requests each get their own
DependencyResolverinstead of sharing one: a concurrent request can no longer clear another in-flight request's pendingyield-dependency teardowns (database session close, file-handle release), which previously could be silently skipped under load. Config.from_env_filestrips an unquoted inline#comment from a.envvalue (KEY=value # note→value) while leaving a#inside a quoted value intact.app.mount()rejects an overlapping prefix registration (a prefix equal to, nested under, or containing an existing mount) withValueError, instead of silently shadowing one mount with another.Request.files()no longer returns duplicateUploadFileentries, nor runs in O(n²), when several files are uploaded under one form field name — it now iterates the form's(key, value)pairs once.StaticFiles._etag_cacheis now a bounded LRU (default cap 1 024 entries per instance, configurable via the class attributeETAG_CACHE_MAX). The previous unbounded dict grew for the lifetime of the worker on a large static tree.LoggingMiddlewareno longer keeps a per-instance dict keyed byid(request). The start timestamp lives onrequest._state, so it cannot leak when a handler raises, and a recycledid()cannot collide with a stale entry to log a nonsensical duration.jsonable_encoder(obj, include=..., exclude=...)forwards the filters into recursive calls —exclude={"password"}now strips the field at every depth, matching the dataclass branch.Signalactually filters by sender. A receiver connected withsignal.connect(fn, sender=X)fires only whensend(X)runs (matched byis, falling back to==). A receiver connected with the defaultsender=ANY_SENDER(sentinel re-exported fromveloce.signals) still fires for every send.UploadFile.read/write/seek/closenow offload the blocking filesystem syscalls to a thread once the spool has rolled over to disk; the cheap in-memoryBytesIOpath stays on the loop.hash_password_async/verify_password_async— async-safe wrappers that run the scrypt KDF on a thread. The synchash_password/verify_passwordare unchanged; calling either from anasync defhandler blocks the loop for ~100 ms, so async handlers should reach for the_asyncvariants. Both are exported from the top-level package.WebSocket._receive_queueis now bounded (defaultmaxsize=64; configurable via therecv_queue_maxsizeconstructor argument). The cap turns the previously-unbounded queue into a backpressure signal: a peer that sends faster than the handler reads now blocks the producer onputinstead of growing the queue without limit.- All four wall-clock perf checks in the test suite —
TestPerformanceAfterFixes(tests/test_async_safety.py),TestNoSyncIOInHotPath(tests/test_async_io.py), andTestPerformance(tests/test_iteration3.py) — are now marked@pytest.mark.perfand excluded from the defaultpytestrun viaaddopts = ["-m", "not perf"]inpyproject.toml. The relative-to-async budget alone was still flaky under full-suite CPU contention; opt in withpytest -m perfon a quiet machine. Catastrophic dispatch regressions remain gated in CI bybench/dispatch_bench.py --min-rps 2000. - Documentation corrected against the code: sync (
def) handlers are documented as supported (run in a thread-pool executor); the built-in development server is documented as HTTP/1.1-only (WebSocket and HTTP/2 workloads run under an external ASGI server); the shippedServerSessionMiddleware/SessionStorereplaces a stale "on the roadmap" note; and scoped request hooks are clarified as aBlueprintfeature, not a plainRouterone.
Performance¶
- Radix-tree param-child lookup is now O(1) at registration via a
(param_name, converter_type)sidecar index (CL40 / CL41). The ordered list remains the source of truth at match time so traversal semantics are unchanged._split_pathalso drops itsstrip("/")pass — the empty-string filter on the split already handles leading, trailing, and consecutive slashes. - Independent sibling
Depends()slots now resolve in parallel viaasyncio.gatherwhen safe (CL10). The resolver looks ahead for contiguousK_DEPENDSsiblings and dispatches them concurrently unless the run contains aSecurity()scope-pushing slot, a yield-style dependency, or two siblings sharing ause_cache=Truecallable — those cases preserve the sequential semantics that protect the resolver's shared cache, security-scope stack, and teardown ordering. Handlers with multiple I/O-bound deps now wait formax(durations)instead ofsum(durations). - Blueprint hooks (
before_request,after_request,teardown_request) are now bucketed by blueprint at registration (CL22). Dispatch reads the bucket for the matched route's endpoint prefix instead of iterating every blueprint's gated wrapper and doing astartswithno-op on each — eliminates the O(B·H) per-request overhead for apps with many blueprints. StaticFilesstreams files at or aboveSTREAM_THRESHOLD(1 MiB default) viaStreamingResponseinstead of buffering the whole body (CL4). Worker RSS no longer grows by the file size for the duration of a large download. Range requests still buffer the slice, which is already bounded by the client.Request.mimetypeandRequest.mimetype_paramsnow share a single cached parse (CL14): the first access populates_parsed_ctwith a(mimetype, params)tuple; subsequent reads on either property hit the cache. Handlers that touchcontent_typerepeatedly (form parsers, validators) stop re-splitting the same string per access._find_exception_handlernow memoises the MRO walk per exception type. The cache is cleared whenever a new error handler is registered so a fresh registration takes effect for previously cached subclasses.SignedSerializer.loadsdoes a singletoken.split(".", 2)instead oftoken.count(".") != 2+token.split("."). The early-validation path is now one pass over the string.LoggingMiddlewareshort-circuits when the logger has the access level disabled — both thetime.monotonic()clock read on entry and the duration calculation on response.SessionMiddleware._cookie/ cookie composition use"; ".join(parts)instead of a chain of+=concatenations, cutting intermediate string allocations.StaticFilesdirectory listing readsis_dirfromos.scandir's cached dirent — saves a per-entryos.path.isdirsyscall. Behaviour note: symlinks inside a listed directory are now classified via the symlink itself, not its target — a symlink to a subdirectory renders as a plain entry rather than a directory entry. This avoids advertising symlink targets in the listing and matches the symlink-safety stance the static handler already takes.Response.encode()no longer rebuilds the header dict on every response: the three framework defaults (Content-Type,Content-Length,Connection) are emitted inline only when the caller has not supplied them. A case-insensitive check at the same time removes a latent duplicate-header bug where a user-supplied"content-type"(lowercase) would land alongside the default"Content-Type"in the encoded line. Reason phrase comes from the module-level{code: phrase}map already added forResponse.status.StaticFilesnow satisfies the existence + stat with a single executoros.statcall, classifying file/dir fromst_mode— the previous request path issuedisfileand then a secondstatfor size/mtime, doubling executor round-trips.- WebSocket
_send_framehands the header + payload to the transport as two separate buffers viawritelinesinstead ofbytearray.extend(data)+bytes(frame). On a 64 KiB frame that saves a 64 KiB memcpy on the way out. - SSE: single-line event payloads skip the
data.split("\n")list allocation; chunked SSE writes usetransport.writelinesinstead of concatenating size-line + body + trailer into one fresh bytes per chunk. - SSE status-line reuses the response-module
_STATUS_PHRASEStable rather thanfrom http import HTTPStatus+HTTPStatus(code).phraseon every stream startup. http_date(None)(the per-responseDate:header) caches the RFC 9110 IMF-fixdate to one-second resolution —formatdate()ran once per response despite the output only changing once a second.- App-level hook dispatch (
teardown_request,teardown_appcontext,_call_handler) readsiscoroutinefunctionfrom a memoised cache keyed byid(fn). Hooks register once and are dispatched many times; the inlineinspect.iscoroutinefunction(...)walk on every request is replaced by a dict lookup. CORSMiddlewareprecomputes", ".join(self.allow_methods),", ".join(self.allow_headers), and", ".join(self.expose_headers)at construction. Per-response preflight emission now hits the precomputed strings instead of rebuilding them on every cross-origin reply.- Cookie-based
SessionMiddlewaredrops thejson.dumps(sort_keys=True)mutation tripwire (computed on entry and on exit) in favour of theSession.modifiedflag theSessioncontainer already maintains — saves two full serialisations on every request that traverses the middleware. - WebSocket frame unmasking is now bulk-XOR via
int.from_bytes/to_bytesover the tiled mask, replacing a Python-level per-byte loop. Saves measurable CPU on any frame past a handful of bytes (WebSocket payloads are usually hundreds to KiB-sized). Response.statusreads the reason phrase from a module-level{code: phrase}map built once at import time, instead of constructing anHTTPStatus(code)IntEnum-walk on every access.email.utils.parsedate_to_datetimeis now imported at module load inveloce.http.requestinstead of re-imported inside four conditional-request hot properties.StaticFilescachesmimetypes.guess_typeper file path (bounded LRU, 512 entries) —guess_typewas running its full MIME table walk on every static hit.safe.secure_filenameuses a module-level compiled regex for the underscore-run collapser, removing the per-callrecache lookup.- Per-request reflection eliminated from the hot path: handler signatures are inspected once at registration into a frozen resolution plan.
- Static route lookup is O(1) per tree level — static child nodes are indexed in a dict instead of scanned linearly, so match cost no longer grows with the number of sibling routes.
- Request dispatch matches each route once per request instead of twice.
- The dependency resolver no longer re-imports its slot-kind constants on
every call, and
_call_handlerskips a per-request coroutine-function probe by reading the precomputed handler plan. StaticFilesresolves the served root's real path once at construction rather than on every request; the request-scopedgstore is allocated lazily, so handlers that never touchgpay no allocation.- A parameter's
pattern/regexconstraint is compiled once at declaration time instead of recompiled on everyvalidatecall. CORSMiddlewareprecomputes its origin allow-list as a frozenset and a lowercased header allow-set at construction, so per-request CORS checks are O(1) instead of scanning a list.Jinja2Templatesauto_reloadnow follows the bound app'sdebugflag when left unset — production rendering skips the per-render templatestatsyscall. Pass an explicitauto_reload=to pin it.response_model=list[Model]dumps a handler-returned element that is already an instance of the target model directly, skipping a re-validation round-trip (and correctly preserving per-elementexclude_unset, matching the scalarresponse_modelpath).- The ASGI entry point decodes request headers via a list comprehension rather than a generator, trimming a per-header generator-frame resume.
- A route whose handler takes no injected parameters and declares no
dependencies is now dispatched through a trivial-route fast path that
skips the dependency resolver entirely instead of resolving to
{}.
Security¶
MAX_CONTENT_LENGTHis now enforced incrementally — an oversized request body is refused with413while still being received, before the whole payload is buffered into memory.- The built-in development server enforces a request-read timeout
(
HttpProtocol.REQUEST_TIMEOUT, 30 s) — a half-sent request is dropped with408, bounding how long a slowloris-style slow client can pin a connection open. WebSocketOriginMiddlewarerejects cross-site WebSocket handshakes (CSWSH) by checking the handshakeOriginagainst an allow-list.SecurityHeadersMiddlewareattachesX-Content-Type-Options,X-Frame-Options,Referrer-Policy, and optional HSTS / CSP /Permissions-Policyresponse headers.CSRFMiddlewareaccepts asecretthat HMAC-signs the token (with an optionalmax_ageexpiry), so a cookie value carrying no valid server signature is refused — raising the bar against cookie-injection CSRF. The CSRF cookie now defaults toSecure.- Multipart form parsing caps the part count and per-part size
(
MAX_FORM_PARTS/MAX_FORM_PART_SIZEconfig keys), raising413— a guard against algorithmic-complexity DoS from a maliciously structured form. app.use_secure_defaults()applies a hardened baseline (secure session cookies +SecurityHeadersMiddleware);app.security_audit()and the newveloce checkCLI command report configuration risks before deployment.