ADR-074: Http* family redesign — wiring-first, distributed-by-default, ADR-062 completion
Date: 2026-07-13 Authors: Jean-Francois Meyers Status: Accepted Scope:
Granit.Bulkhead(+.Wolverine),Granit.Http.Bulkhead,Granit.Http.UrlSafety(renamed fromGranit.Http.Security),Granit.Http.Hosting(absorbsGranit.Http.Cors+Granit.Http.ResponseCompression),Granit.Http.Idempotency(+ new.Abstractions),Granit.RateLimiting,Granit.Http.Cookies.CookieConsent(Klaro deleted),Granit.Diagnostics.Endpoints(absorbsGranit.Http.SecurityHeaders.Endpoints),Granit.Http.Abstractions(deleted)
Context
Section titled “Context”A systematic audit of the Granit.Http* family (epic
#2986) found that the
family had grown by accretion — one package per feature, added when the feature
was written — and that several packages were wired green but functionally
dead:
- Dead-but-green wiring. Modules registered services and shipped options
classes whose configuration was never bound: a
SectionNameconstant existed, tests asserted the constant’s value, but noBindConfigurationcall ever read the section.Http:Idempotencywas the canonical case — every key under it was silently ignored. A green test suite proved the constants compiled, not that the feature worked. - Decorative SectionNames. The convention “every options class declares
SectionName” had become a cargo cult: the constant satisfied reviewers while the binding was absent. Conventions that are not machine-enforced decay into decoration. - Silently-wrong distributed behavior. The idempotency store and the rate-limiting counter store fell back to in-memory implementations when no Redis cache was registered. In a multi-replica deployment that turns “at-most-once” into “at-most-once per pod” and every rate limit into N × limit — the worst kind of bug: invisible until an incident.
- Package-boundary drift.
Granit.Http.Bulkheadpredated ADR-062 and still carried its algorithm inside the ASP.NET Core binding.Granit.Http.Securitywas a misleading name for URL validation.Granit.Http.CorsandGranit.Http.ResponseCompressionwere two always-referenced-together single-feature packages.Granit.Http.Abstractionsheld two unrelated leftovers.Granit.Http.Cookies.Klarowas deprecated but still shipping.Granit.Http.SecurityHeaders.Endpointswas a whole package for one diagnostics endpoint.
Decision
Section titled “Decision”Three principles, applied across the family in one restack (#3025):
1. Wiring-first
Section titled “1. Wiring-first”A feature exists only if its wiring is provable: options must be bound from
their declared section, middleware must be applied (or auto-applied), and
the proof is an architecture test, not a code review. Http:Idempotency is now
actually bound; CORS middleware is auto-applied via IStartupFilter
(opt-out Http:Cors:AutoRegisterMiddleware = false) so a configured-but-never-
enforced policy can no longer ship.
2. Distributed-by-default, fail-loud
Section titled “2. Distributed-by-default, fail-loud”Middleware whose correctness depends on shared state refuses to start with a
per-process store outside Development:
- Idempotency: a non-distributed
IConditionalCachefails startup unlessHttp:Idempotency:AllowInMemoryStore = true. - Rate limiting: an in-memory counter store fails startup unless
RateLimiting:AllowInMemoryCounterStore = true.
The opt-in flags exist for single-replica hosts and are documented as such. A loud startup failure is cheaper than a silent double payment.
3. Ratchet architecture tests
Section titled “3. Ratchet architecture tests”Every fixed class of defect gets a convention test that fails on regression:
options-binding coverage (OptionsBindingConventionTests — every SectionName
must be bound), section-name alignment (SectionNameConventionTests —
namespace-aligned, colon-separated), and metrics wiring
(MetricsWiringConventionTests — every *Metrics class registered and its
meter reachable). The tests are ratchets: exemption lists may only shrink.
Package moves
Section titled “Package moves”| Was | Now | Kind |
|---|---|---|
Granit.Http.Bulkhead (monolith) | Granit.Bulkhead (framework-pure core: registry, quota providers, options, diagnostics, 18-culture localization) + Granit.Bulkhead.Wolverine ([Bulkhead] + convention middleware, no WolverineFx ref) + slimmed Granit.Http.Bulkhead (.RequireGranitBulkhead() + RFC 7807 503 mapper) | ADR-062 split |
Granit.Http.Security | Granit.Http.UrlSafety (module GranitHttpUrlSafetyModule, meter Granit.Http.UrlSafety, no tenant_id tag on counters) | Rename |
Granit.Http.Cors + Granit.Http.ResponseCompression | Granit.Http.Hosting (config sections unchanged) | Consolidation |
Granit.Http.Cookies.Klaro | Deleted — UseCookieConsent() on GranitCookiesBuilder is the single public CMP entry point | Deletion |
Granit.Http.SecurityHeaders.Endpoints | Deleted — MapGranitSecurityHeadersAudit() moved to Granit.Diagnostics.Endpoints (Diagnostics:Endpoints:SecurityHeadersAudit) | Fold |
| — | Granit.Http.Idempotency.Abstractions ([Idempotent] + IIdempotencyMetadata, namespace Granit.Http.Idempotency) — *.Endpoints packages reference the contracts, never the middleware | New contracts package |
Granit.Http.Abstractions | Deleted — dissolved into the idempotency contracts package; MinimumResponseTimeGuard moved internal to Granit.Identity.Local.Endpoints (its only consumer) | Dissolution |
Breaking configuration/telemetry moves: section Http:Bulkhead → Bulkhead;
meter/ActivitySource Granit.Http.Bulkhead → Granit.Bulkhead; metrics
granit.http.bulkhead.* → granit.bulkhead.*.
Alternatives considered
Section titled “Alternatives considered”A. Fix the wiring bugs without moving packages
Section titled “A. Fix the wiring bugs without moving packages”Rejected. The wiring bugs were symptoms; the accretion-shaped package graph was
the cause. Leaving Granit.Http.Bulkhead monolithic, or the CMP with two entry
points, reproduces the same drift within a release or two.
B. Warn instead of failing on in-memory stores
Section titled “B. Warn instead of failing on in-memory stores”Rejected. A startup warning in a container log is read by nobody until the incident review. The failure mode being guarded against (double side effects, N × rate limits) is silent and financially consequential — the guard must be as loud as the bug is quiet. Development environments and explicit opt-in flags cover every legitimate in-memory scenario.
C. One-off cleanup without ratchet tests
Section titled “C. One-off cleanup without ratchet tests”Rejected. This audit is the second time decorative wiring was found in the family. Without machine enforcement the third time is a certainty.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- Misconfiguration is now loud: unbound sections and per-process stores fail the build or the startup, not the incident review.
- The Http* family reads as a system: hosting concerns in
Http.Hosting, contracts in*.Abstractions, algorithms in framework-pure cores, one CMP entry point, diagnostics endpoints under diagnostics. - Net package count −1 (6 deleted, 5 created) despite three new packages — consolidation paid for the splits.
Negative
Section titled “Negative”- Breaking changes (pre-1.0): the
Bulkheadsection/meter/metric renames, theGranit.Http.UrlSafetynamespace moves, and the deleted packages require consumer updates. Migration tables ship on the Bulkhead, URL Safety, HTTP Hosting and Klaro migration pages. - Hosts that relied on the silent in-memory fallbacks must now either provision Redis or set an explicit opt-in flag.
Neutral
Section titled “Neutral”- Apps consuming the family through
Granit.Bundle.Essentials/Granit.Bundle.Apipick up the moves transitively — mostlyusingchanges. - ADR-055 keeps its historical package name (
Granit.Http.Security); ADRs are never retro-edited.
References
Section titled “References”- Epic #2986 — Http* family redesign audit + stories
- PR #3025 — restack landing the family in one pass
- ADR-062 — Framework-pure core + transport bindings — the layering convention completed here
- ADR-055 — Extract URL safety + temp-file primitives — birth of the package renamed to
Granit.Http.UrlSafety - Bulkhead / Rate Limiting / Idempotency / HTTP Hosting / URL Safety — updated reference pages