fastware v0.6.0 /Changelog
On this page

#Changelog

#Unreleased

#Fixes

  • **stop() no longer reports a killed server as running.** After escalating to SIGKILL it now waits for the kill to actually land, so status(), list_instances() and check_already_running() called right after a stop tell the truth instead of seeing the corpse's PID.

#0.6.0

Graceful background-server shutdown, two new documentation guides, and a declared strictcli floor.

Context

The release's headline fix is in the server lifecycle. A server started with serve_background() is a direct child of the caller, so on exit it stayed a zombie in the caller's process table -- and every liveness probe in the module is kill(pid, 0), which succeeds for zombies. stop() therefore never terminated anything gracefully: it polled a server that was already gone for its whole 10-second grace window and then escalated to SIGKILL, every single time. status(), list_instances() and check_already_running() lied in the same way. The probes now reap their own exited children first.

This visit also adopts the stricttest test-isolation floor. The loopback stance is "allow" rather than an exact allowlist because this suite boots real servers on ("127.0.0.1", 0) -- the kernel picks the port at bind time, so there is no host:port pair for an allowlist to name at configuration time. Off-machine egress stays denied. Four tests whose spawned child re-imports its target off the inherited repo cwd carry @pytest.mark.repo_cwd.

#Features

  • Two new documentation guides. An observability guide (request IDs, timing middleware, the error log and the audit log) and a tasks-and-features guide (background task lifecycle and feature flags), plus live extras and middleware tables that are generated from the source rather than hand-maintained.

#Fixes

  • **The fastware dev CLI works again on current strictcli.** It was pinned three releases behind and hard-errored at registration under 0.36.0; all three commands now declare their effect classification and use the framework's ctx-first handler signature.
  • **fastware now declares strictcli>=0.36.0.** The CLI's effect classification requires it, so an environment resolving an older strictcli used to hard-error at command registration instead of being fixed by the resolver.
  • **stop() now shuts a background server down gracefully instead of always killing it.** A server started with serve_background() became a zombie in the caller's process table on exit, and every liveness probe (kill -0) reported it as still running -- so stop() waited out its full 10-second grace window and escalated to SIGKILL every time, status() reported a dead server as running, list_instances() kept returning it, and check_already_running() refused to start a replacement.

#0.5.0

Streaming client-disconnect detection, the /__fastware/ runtime namespace (version endpoint, cache headers, blessed service worker, live update channel), an instance registry with presence markers, and the file-driven fastware dev CLI.

Context

This release rounds out fastware's runtime and dev-tooling story.

Streaming handlers previously leaked resources (DB sessions, queues) when a client walked away mid-stream: their finally cleanup never ran. A single receive-owning watcher now cancels abandoned stream generators so cleanup runs, and request.is_disconnected() is reliable in both streaming and non-streaming handlers.

Apps now expose a reserved /__fastware/ runtime namespace: a version endpoint returning an opaque content-hashed build id, immutable cache headers for hashed assets (no-cache for index/SPA fallback), an explicit sw_mode (cache|reset|off) blessed service worker with foreign-worker detection and a mandatory cache -> reset -> off retirement route, and a live update channel (SSE at /__fastware/events plus a dependency-free client that performs one state-preserving reload when the build id changes).

An instance registry lets peers enumerate running servers via list_instances() with automatic stale-entry pruning, plus a lightweight presence-marker API (write_marker/list_markers/remove_marker) for cross-process signals such as window refcounting and focus requests.

Finally, fastware dev run|status|stop drives a file-driven [tool.fastware.dev] Vite + backend development environment with pre-spawn gates, health-gated aux services, topology ordering, process-group supervision, and an optional daemon mode.

#Features

  • Version endpoint and static-asset caching. Every app now serves /__fastware/version returning an opaque build_id (a content hash of the static assets, stable across mtime changes) plus the app name when configured. Static serving also gained cache headers: hashed Vite-style assets (name-<hash>.ext) get Cache-Control: public, max-age=31536000, immutable, while unhashed assets, index.html, and the SPA fallback get no-cache.
  • Instance registry. Server instances now register a JSON descriptor (PID, port, name) so peers can enumerate running instances via list_instances(), with register_instance()/deregister_instance() for lifecycle and automatic stale-entry pruning on read.
  • Blessed service worker. Apps that serve a frontend now choose an explicit sw_mode (cache, reset, or off). cache serves a generated worker at /__fastware/sw.js (per-build cache name, network-first shell, cache-first hashed assets, never intercepting API/SSE/WebSocket/reserved paths) plus a /__fastware/sw-register.js snippet with foreign-worker detection; reset serves a self-destruct worker at both the reserved path and legacy registration paths (legacy_sw_paths) to free clients with a stale worker; off disables it. sw_mode is mandatory with a frontend and forbidden for API-only apps.
  • Live update channel. Every app now exposes an SSE update channel at /__fastware/events (each connection primed with the current build id) and a dependency-free /__fastware/client.js that reloads the page once when the build id changes, preserving form fields and app-registered state across the reload.
  • fastware dev CLI. New fastware dev run|status|stop commands drive a file-driven [tool.fastware.dev] Vite + backend development environment: pre-spawn gates, health-gated aux services, topology ordering (backend-first/vite-first), process-group supervision with graceful-then-kill teardown, and an optional --daemon mode registered in the instance registry.
  • Presence-marker registry API. New write_marker/list_markers/remove_marker helpers on the instance registry for lightweight per-file cross-process signals (e.g. window refcounting, focus requests) that live beside instance descriptors.

#Fixes

  • Streaming disconnect detection. Streaming handlers now detect client disconnects: a single receive-owning watcher cancels abandoned stream generators so their finally cleanup runs, fixing resource leaks (DB sessions, queues) on streams the client walked away from. request.is_disconnected() is reliable in streaming and non-streaming handlers, and DI-provided resources stay alive for the whole stream body.
  • Service worker cache-retirement guidance. The sw_mode validation error and docs now spell out the mandatory cache -> reset -> off migration route, warning that switching a cache worker straight to off strands clients (a 404'd worker script never unregisters).
  • dev SW-mode guard notice. fastware dev run now prints a one-line stderr notice when it skips the service-worker cache guard for a cmd-form backend (which cannot be introspected for sw_mode), instead of skipping silently.
  • Granian embed stability. The embedded Granian server is pinned to a compatible API range (>=2.7,<3.0) to guard against upstream breaking changes, and no longer emits Granian's experimental-API warning at startup.

#0.4.0

Embedded async server for background mode with graceful shutdown

Context

The old foreground=False path ran Granian.serve() in a daemon thread, which had fork-in-thread, sys.exit-in-thread, and no-stop-mechanism bugs. The new implementation uses granian.server.embed.Server, running async workers without forking or signal registration.

#Features

  • New feature. Background serve (foreground=False) now uses embedded async server, fixing hang/stop/fork issues. New stop_background(url) function cleanly shuts down background servers.

#0.3.1

Fix CI so the 0.3.1 publish gate can pass: 0.3.0 was blocked because test dependencies (pyjwt, bcrypt, structlog, watchfiles, websockets, mcp, pydantic) were missing from the dev group and CI's uv sync --locked never installed them.

#Infrastructure

  • Fix CI so the 0.3.1 publish gate can pass: 0.3.0 was blocked because test dependencies (pyjwt, bcrypt, structlog, watchfiles, websockets, mcp, pydantic) were missing from the dev group and CI's uv sync --locked never installed them.

#0.3.0

Server, testing, and serialization hardening: pinned event loop with loop/workers config on serve(), test-client exception re-raise plus pytest-collection fix, msgspec-native json_as, and a broad sweep of ASGI correctness, auth, middleware, SSE, WebSocket, routing, DI, and static-file fixes.

Context

Granian's default loop auto-selection silently switches the event loop when rloop/uvloop are installed, which is a behavior-change hazard for subprocess-based workloads. serve()/serve_background() now pin asyncio by default and expose explicit loop/workers configuration so the runtime is predictable and opt-in.

This release also finalizes a large hardening pass across the framework (auth, CORS/TrustedHost/Vite middleware, SSE and WebSocket handling, lifespan, dependency injection, static-file traversal, request body limits, and HTTP method semantics) plus internal refactoring that extracted shared scope/header/file-writer helpers. Several defaults changed in security-hardening ways (query-param JWT is now opt-in; the internal ParsedSegment type left the public API), which land as a minor bump under 0.x semver.

#Breaking

  • check_already_running() no longer accepts the unused name parameter
  • Breaking. fastware.mcp no longer ships built-in ROLES/DEFAULT_ROLE; register_tools_for_role and create_mcp_server now require an explicit roles mapping, the SA_ROLE env fallback is gone, and the mcp package is imported lazily (faster import fastware.mcp)
  • **Pinned event loop on serve()/serve_background().** New loop parameter ("asyncio" default, or "uvloop"/"rloop") and workers parameter. The effective default changes from Granian's auto (which silently switches loops when rloop/uvloop are installed) to a pinned asyncio, keeping subprocess-based workloads predictable.
  • **raise_server_exceptions on the test clients.** AsyncTestClient/TestClient now re-raise unhandled handler exceptions into the test by default (matching Starlette). Pass raise_server_exceptions=False to restore the previous behaviour where exceptions become a 500 response.
  • Query-param JWT acceptance is now opt-in via allow_query_token (default off); tokens supplied in the query string are rejected unless explicitly enabled.
  • Breaking. The internal ParsedSegment type is no longer exported from the top-level fastware package; import it from fastware.routing if you still need it.

#Features

  • **request.json_as now decodes msgspec.Struct bodies natively.** When the target type is a msgspec.Struct subclass, json_as decodes via msgspec (the framework's fast path); other types still use Pydantic. Decode failures raise HTTPError(422) in both branches.
  • Dev/Vite proxy improvements: ViteDevProxy streams HTTP responses (honoring backend_prefixes) instead of buffering, /ws WebSocket upgrades route to the backend in dev mode via the newly exposed backend_prefixes option, and proxy WebSocket failures close with code 1011 instead of being swallowed.
  • HTTP method handling: unmatched methods return 405 with an Allow header, HEAD is served through the matching GET handler with an empty body, and allowed_methods is exposed for 405 responses.
  • Request bodies are capped at a configurable max_body_size (returning 413 when exceeded), and body chunks accumulate without O(n^2) concatenation.
  • ASGI lifespan support: startup/shutdown events forward to mounted sub-apps with merged lifespan state, lifespan.startup.failed/lifespan.shutdown.failed are emitted on errors, and the test clients run the full lifespan protocol.
  • Performance: query strings are parsed once and cached, handler accepted-parameter sets are precomputed at app creation (no per-request inspect.signature), and single-header lookups use a linear scan instead of building a full header dict.
  • Error-log SQLite writes are offloaded to a background worker thread, so recording errors no longer blocks the event loop.

#Fixes

  • serve_background readiness no longer requires a /health route: any HTTP response (including 404) counts as server-ready
  • **Callable targets now work with serve_background and serve(reload=True).** Module-level callables are materialized as importable shim modules for the spawned server process; non-importable callables (locals, lambdas, __main__) raise a clear error immediately instead of crashing the child
  • ensure_port_available no longer kills a port holder based on its /health response body; it only stops a process whose PID matches the server's own PID file
  • stop() now signals the server's whole process group (when the server leads it), so granian workers terminate with the main process
  • A foreground serve() after a background serve() in the same process no longer silently loses SIGTERM/SIGINT handling
  • Single-instance serve() creates the PID file atomically (O_CREAT|O_EXCL), so two concurrent starts can no longer both pass the already-running check
  • fastware.types declares __all__ again -- from fastware.types import * no longer leaks Any/Awaitable/Callable/annotations
  • **No more PytestCollectionWarning from TestClient.** Both test client classes set __test__ = False, so importing TestClient into a test module no longer emits a pytest collection warning.
  • SSE hardening: subscriber queues register on iteration start (no leak), multi-line event data is emitted as repeated data: lines, and event data is serialized via msgspec.
  • WebSocket handlers close with the correct codes (1008/1011) on dependency failures, and accept() handles a client disconnect before the handshake instead of sending a spurious accept.
  • Auth hardening: verify_token catches only jwt.InvalidTokenError, hash_password rejects passwords over the bcrypt 72-byte limit, user-store writes are atomic with locked read-modify-write, and rate_limit evicts stale buckets while preserving the handler signature.
  • CORS middleware rejects a wildcard origin combined with credentials, adds Vary: Origin, and gates preflight handling on Access-Control-Request-Method.
  • TrustedHost validates the Host header on WebSocket scopes, rejecting disallowed hosts with close code 1008.
  • Response and disconnect handling: never emit a second http.response.start after streaming begins, is_disconnected no longer drops messages or masks errors, and exception-handler dispatch is guarded when receive() fails before a request is bound.
  • Dependency injection fixes: generator-dependency cleanup errors are logged instead of silently swallowed, dependencies that yield more than once raise RuntimeError, and factories with only keyword-only parameters are no longer mis-called with a positional request.
  • Static file serving: fixed a path-traversal hole where sibling-prefix directories escaped the startswith guard, and file reads (static, FileResponse, SPA) are offloaded via asyncio.to_thread.

#0.2.0

ASGI sub-app mounting via Router.mount(), WebSocketDisconnect exception, benchmark suite, and a new documentation site.

Context

Router.mount() enables composing routers and ASGI sub-apps under path prefixes; downstream consumers already depend on it and are broken against registry 0.1.0. WebSocketDisconnect adds explicit disconnect detection to WebSocket receive methods. The docs site (guides, API reference, comparisons) is generated by selfdoc and deployed via Cloudflare Pages.

#Features

  • New. ASGI sub-app mounting via Router.mount() for composing multiple routers.
  • New. WebSocketDisconnect exception and automatic disconnect detection in WebSocket receive methods.
  • New. Benchmark suite comparing fastware vs FastAPI (serialization, import time, throughput).
  • New. Documentation site (guides, quickstart, API reference, framework comparisons) deployed via Cloudflare Pages.

#0.1.0

Initial release of fastware, a fast batteries-included ASGI framework.

#Features

  • Initial release. ASGI micro-framework: Router, Request/Response types, WebSocket, SSE broadcaster, middleware (CORS, RequestID, RequestTiming, TrustedHost, ViteDevProxy), Granian server lifecycle, dependency injection, auth (JWT, passwords, CSRF), structured logging, background tasks, feature flags, MCP server support, and test client utilities.
Search