Skip to content

1.1.0

Latest

Choose a tag to compare

@lesnik512 lesnik512 released this 05 Jun 19:00
· 20 commits to main since this release
558b1d5

lite-bootstrap 1.1.0 — Lifecycle hardening, config validation, CI gate

1.1.0 is a minor release. No intentional public-API breakage. The two behavior changes that could affect existing code are fixes to genuine bugs and are called out in Behavior changes below.

This release closes a 26-finding bug-audit cycle (audits + retro live under planning/specs/2026-06-05-bug-audit-v2*.md). The changes split across four shipped PRs:

  • #108 — Lifecycle & teardown correctness (10 findings)
  • #109 — Config UX & security validation (6 findings)
  • #110 — Hygiene + CI gate (4 findings)
  • #111 — Generalized TeardownError aggregation + cascade tests + README lifecycle docs (3 deferred follow-ups)

Test suite grew from 153 → 194 (+27%) at 100% line coverage throughout. pip-audit now runs on every PR and weekly via cron; a new filterwarnings config catches accidental InstrumentSkippedWarning emissions.

New features

  • Injectable prometheus_collector_registry on FastStreamConfig. Pass an existing prometheus_client.CollectorRegistry to expose counters registered elsewhere through FastStream's /metrics endpoint. Defaults to a fresh per-instance registry — fully backward compatible.
  • opentelemetry_excluded_urls field on FastStreamConfig. Was a getattr fallback before; now a discoverable, IDE-completable config field matching FastAPIConfig and LitestarConfig.
  • SentryInstrument.teardown(). Calls sentry_sdk.flush(timeout=2) then sentry_sdk.init() (no args) to reset the SDK to a no-op state. Previously the SDK stayed globally configured after bootstrapper teardown, leaking state across process-local tests.
  • FastStreamLoggingInstrument.teardown(). Restores broker.config.logger.params_storage to its pre-bootstrap value. The bootstrap mutated broker state; teardown didn't reverse it.

Bug fixes

Lifecycle & teardown (PR #108)

  • OpenTelemetry teardown now flushes spans and shuts down the tracer provider (LOG-1, LOG-2). bootstrap() stored the TracerProvider only as a local; teardown() couldn't reach it to call shutdown(). Buffered spans in BatchSpanProcessor were dropped on graceful shutdown. Teardown also restores the two OTel-namespace stdlib loggers (opentelemetry.instrumentation.instrumentor, opentelemetry.trace) to their pre-bootstrap disabled state.
  • LoggingInstrument.teardown() runs all cleanup steps even on partial failure (LOG-3). A raise from any handler.close() previously left remaining handlers attached, skipped the root-level reset, and never called close_handlers() on the memory factory. Now wrapped in try/finally with per-handler error capture; all collected errors raise together via TeardownError(errors).
  • LitestarOpenTelemetryInstrumentationMiddleware cache evicts dead refs (LOG-6). The old dict[int, ASGIApp] keyed by id() never evicted, holding wrapper OpenTelemetryMiddleware instances alive after Litestar dropped next_app. Replaced with weakref.WeakKeyDictionary; non-weakrefable apps fall through to the un-cached path.
  • Double-bootstrap on the same application is now detected and warned (LOG-7, LOG-8). Constructing two FastMcpBootstrappers around the same FastMCP (or two FastAPIBootstrappers around the same FastAPI) previously stacked teardown hooks. The second construction now emits a UserWarning, skips the re-attachment, and tells you the second bootstrapper's teardown() won't fire on ASGI shutdown.
  • Generalized TeardownError aggregation across all instruments (PR #111). OpenTelemetryInstrument, FastStreamLoggingInstrument, and SentryInstrument now run their full cleanup sequence even if an early step raises — aggregating errors into a single TeardownError or letting super().teardown() run via try/finally. Previously a misbehaving instrumentor / broker / Sentry flush silently skipped subsequent cleanup.
  • Pyroscope's precondition check survives python -O (LOG-5/SEC-4). Replaced two assert statements (in _narrow_app and PyroscopeInstrument.bootstrap) with explicit raise TypeError(...) and raise RuntimeError(...). python -O strips asserts; the invariants now hold under all optimization levels. Closes the two bandit B101 findings.

Config & security (PR #109)

  • FastAPIConfig no longer stomps user-supplied app's title/debug/version (UX-1). Previously the three assignments ran unconditionally; a user passing FastAPI(title="My API", version="3.0.0") would silently have those clobbered by lite-bootstrap defaults. Now the assignments only run in the UnsetType branch (when lite-bootstrap constructed the app). See Behavior changes below.
  • enable_offline_docs validates request.scope["root_path"] against the existing path allowlist (SEC-1). Invalid root paths (e.g., HTML-injection payloads via a malicious upstream proxy's X-Forwarded-Prefix) now fall back to empty and emit a warning instead of being reflected into Swagger/Redoc HTML script tags. Threat model: not an issue in default ASGI deployments; only matters if ProxyHeadersMiddleware trusts upstream prefix headers.
  • OpenTelemetry endpoint with insecure=True emits a warning for non-local hosts (SEC-2). New OpenTelemetryConfig.__post_init__ parses the endpoint (handles both host:port and scheme://host:port forms, including IPv6 brackets and unix://) and warns when traces would ship unencrypted to a non-localhost/127.0.0.1/::1/unix:// target.
  • CorsConfig rejects unsafe wildcard + credentials combos at construction (SEC-3). cors_allowed_credentials=True combined with cors_allowed_origins=["*"] (or a permissive regex like ".*"/".+") is the canonical CORS misconfiguration — browsers reject the response. Now raises ConfigurationError immediately instead of silently building a non-functional CORS layer. See Behavior changes below.

Hygiene & process (PR #110)

  • Missing-dependency events now log via stdlib logging in addition to warnings.warn (UX-4). Users running under python -W ignore or PYTHONWARNINGS=ignore previously saw nothing when a configured instrument's optional dep was missing. The new logger.warning line on lite_bootstrap.bootstrappers.base is unaffected by warning filters.

Behavior changes

Two changes could affect existing code in ways the previous release wouldn't have. Both fix real bugs; surfaced here so you can audit.

  1. User-supplied FastAPI instance retains its own title, debug, version. If you were relying on lite-bootstrap to overwrite these from service_name/service_debug/service_version after handing it a pre-built FastAPI(), you'll now see your original values. Migration: set these on your FastAPI() directly, or use FastAPIConfig(application_kwargs={...}) to have lite-bootstrap construct the app.

  2. CorsConfig(cors_allowed_origins=["*"], cors_allowed_credentials=True) now raises ConfigurationError. This combo was never functional — browsers reject responses with Access-Control-Allow-Credentials: true and Access-Control-Allow-Origin: *. If your code constructed this combo and worked anyway (because the credentials header was silently dropped by FastAPI's CORSMiddleware), construction now fails with a clear message. Migration: enumerate allowed origins explicitly, or set cors_allowed_credentials=False.

New constraints documented

Three lifecycle constraints surfaced by the audit are now documented in README.md and CLAUDE.md:

  • One bootstrapper per application instance. Second construction emits a warning and skips re-attachment (see LOG-7/LOG-8 above).
  • One OpenTelemetryInstrument per process. The OTel SDK enforces set_tracer_provider as set-once via _TRACER_PROVIDER_SET_ONCE.do_once(...) (verified against opentelemetry/trace/__init__.py:548-556); teardown() cannot reset the global pointer.
  • __post_init__ cascade invariant. Every config-class __post_init__ must call super().__post_init__(). BaseConfig ships a no-op as the chain terminator. FastAPIConfig uses the explicit super(FastAPIConfig, self).__post_init__() form because @dataclass(slots=True) breaks bare super().

CI changes

  • security-audit.yml workflow added. pip-audit runs on every PR and weekly via cron against the lockfile (uv export --all-extras --no-hashes). The default-branch run is purely informational; PRs that introduce CVEs will block until resolved.
  • InstrumentSkippedWarning escalated to error in tests. Any unexpected emission outside a pytest.warns(...) block now fails the test (registered via pytest_configure() in tests/conftest.py; can't live in pyproject.toml because that import order breaks pytest-cov tracing).

Backwards compatibility

Aside from the two Behavior changes called out above, every public API behaves identically. New fields default to their old behavior (prometheus_collector_registry=None → fresh registry as before; opentelemetry_excluded_urls=[] → empty set as before). New warnings/validators trigger only on configurations that were already broken or risky.

The 26-fix list with full file:line references and rationale is in:

  • planning/specs/2026-06-05-bug-audit-v2.md — the audit
  • planning/specs/2026-06-05-bug-audit-v2-sequencing.md — the 3-PR breakdown
  • planning/specs/2026-06-05-bug-audit-v2-retro.md — what the cycle taught us

References