8.9 KiB
Changelog — einherjar/web
All notable changes to this module are documented here. Format follows Keep a Changelog. This module adheres to Semantic Versioning.
[1.1.2] — 2026-08-08
Patch — CORS wildcard hardening plus documentation fixes.
Changed
mw.CORSnow rejects"*"(panics at construction) instead of silently no-op'ing it."*"matched nothing (exact-match only), so a service passing it ran with CORS effectively off — a silent trap. Fail loud at boot; usemw.CORSAllowAll()(development) or list explicit origins.- Bumped
contracts,coreto v1.1.2.
Fixed
- README Go examples now compile:
mw.Recover(logger),health.NewHandler(...).ServeHTTP, and themw.CORSexample no longer passes"*". Corrected theCORSAllowAlldescription.
[1.1.1] — 2026-08-07
Patch — coordinated framework version alignment.
Changed
- Bumped
contractsandcoreto v1.1.1 (framework version alignment). No code or API changes.
[1.1.0] — 2026-08-07
Coordinated framework release. Documentation fixes plus the framework version bump
(which finally makes the previously-drafted contracts v1.1.0 pin real).
Fixed
- Package doc examples didn't compile. Verified by compiling the example patterns
against the real API:
mw.Recover()->mw.Recover(logger)— the recover middleware takes alogging.Logger(server,mwpackage docs andserver.go).health.NewHandler(logger, …)->health.NewHandler(logger, …).ServeHTTP— the handler returnshttp.Handler, but chi'sGettakeshttp.HandlerFunc; same forNewHandlerWithConfig(server,web,healthpackage docs).
Changed
- Bumped
contractsandcoreto v1.1.0 (framework version alignment).
1.0.0 — 2026-05-28
Added
server
Serverinterface — embedslifecycle.Component(fromcontracts/lifecycle) andchi.Router(fromgo-chi/chi/v5); any type that satisfies both is directly compatibleConfigstruct —Host,Port,ReadTimeout,WriteTimeout,IdleTimeout,ShutdownTimeout; all fields carryenv:"EINHERJAR_SERVER_*"andenvDefaulttags (caarlos0/envsyntax)New(logger logging.Logger, cfg Config, opts ...Option) Server— constructs the unexportedimplstruct; embedschi.NewRouter()Optiontype +WithMiddleware(mw ...func(http.Handler) http.Handler) Option— variadic option for middleware compositionimpl.OnInit()— applies registered middleware viachi.Useimpl.OnStart()— binds TCP listener synchronously (net.Listen), startshttp.Server.Servein a goroutine; port binding failure returns immediatelyimpl.OnStop(ctx)— gracefulhttp.Server.Shutdown(ctx)withShutdownTimeout(fallback:defaultShutdownTimeout = 10s)var _ Server = (*impl)(nil)— compile-time assertion
mw
StatusRecorderstruct — wrapshttp.ResponseWriter, captures written status codeRecover() func(http.Handler) http.Handler— catches panics, writes 500, logs stack trace viaruntime/debug.Stack()RequestID(generator func() string) func(http.Handler) http.Handler— injects a request ID vialogz.WithRequestID; reads existingX-Request-IDheader if presentRequestLogger(logger logging.Logger) func(http.Handler) http.Handler— structured request logging: method, path, status, latency; usesStatusRecorderto capture codeCORS(origins []string) func(http.Handler) http.Handler— setsAccess-Control-Allow-Originfor listed origins; supports preflight (OPTIONS)CORSAllowAll() func(http.Handler) http.Handler— allows any origin by reflecting the requestOrigin(noAccess-Control-Allow-Credentials); development onlyRateLimiterStoreinterface —Allow(ctx context.Context, key string) (bool, error); pluggable backend;errorreturn allows infrastructure failures to surface; fail-open contract: non-nil error allows the requestInMemoryRateLimiterStorestruct — per-key token bucket viagolang.org/x/time/rate;sync.Mapfor concurrent access; background goroutine evicts idle entries after 5 minutes viatime.Ticker;Allowalways returns(bool, nil)NewInMemoryRateLimiterStore(rps float64, burst int) *InMemoryRateLimiterStoreIPRateLimit(store RateLimiterStore, logger logging.Logger) func(http.Handler) http.Handler— limits by client IP (X-Forwarded-For→RemoteAddrfallback); returns 429 JSON on exceeded limit; fails open on store errorUserRateLimit(store RateLimiterStore, logger logging.Logger) func(http.Handler) http.Handler— limits by authenticated user ID fromsecurity.FromContext; falls back to client IP when no identity present; same 429 + fail-open behaviour
httputil
HandlerFunctype —func(w http.ResponseWriter, r *http.Request) error; implementshttp.HandlerviaServeHTTPHandle[Req, Res any](v valid.Validator, fn func(ctx context.Context, req Req) (Res, error)) http.HandlerFunc— decodes JSON body, validates struct, callsfn, encodes response; 400 on validation failure, mapped status on*xerrors.ErrHandleNoBody[Res any](fn func(ctx context.Context) (Res, error)) http.HandlerFunc— no body decoding/validation; encodes response directlyHandleEmpty[Req any](v valid.Validator, fn func(ctx context.Context, req Req) error) http.HandlerFunc— decodes and validates body, callsfn, returns 204 on successJSON(w http.ResponseWriter, status int, v any)— writes JSON responseNoContent(w http.ResponseWriter)— writes 204 with no bodyError(w http.ResponseWriter, err error)— maps*xerrors.Errto HTTP status and writes{"code":"<wire_value>","message":"<msg>"}JSON body; complete 16-code mapping
health
Configstruct —CheckTimeout time.Durationwithenv:"EINHERJAR_HEALTH_CHECK_TIMEOUT" envDefault:"5s"(caarlos0/envsyntax)Responsestruct —Status string,Components map[string]ComponentStatusComponentStatusstruct —Status,Latency(omitempty),Error(omitempty)NewHandler(logger logging.Logger, checks ...observability.Checkable) http.Handler— shorthand with default 5s timeoutNewHandlerWithConfig(logger logging.Logger, cfg Config, checks ...observability.Checkable) http.Handler— all checks run concurrently in goroutines with a shared context timeout; results collected via buffered channel;DOWN(critical priority) → 503;DEGRADED(degraded priority) → 200;UP→ 200- Accepts
observability.Checkablefromcontractsdirectly — no local redefinition
Root package (web)
Configstruct —Server server.Config,AllowedOrigins []stringwithenv:"EINHERJAR_SERVER_CORS_ORIGINS" envSeparator:","New(logger logging.Logger, cfg ...Config) server.Server— pre-wires recommended middleware stack: Recover → RequestID (UUID v7 with v4 fallback) → RequestLogger → CORS (only whenAllowedOriginsnon-empty)- Unexported
newRequestID()— usesuuid.NewV7()(time-ordered), falls back touuid.NewString()(v4) on generation error
Design Notes
-
Progressive disclosure.
web.Newis the happy path — one call, all middleware pre-wired, env vars respected.server.Newis the escape hatch — every choice explicit. Both tiers share the same config structs, env variables, and lifecycle contract. -
RateLimiterStoreinterface. The pluggable backend design lets developers start withInMemoryRateLimiterStore(zero extra dependencies) and swap to a distributed store (e.g.,cache-valkey) at scale without touching middleware wiring. The store satisfies the interface via Go duck typing —cache-valkeynever importsweb/mw. -
Fail-open rate limiting. When the store returns an error (e.g., Valkey unavailable), the request is allowed. Availability is preferred over hard enforcement during infrastructure degradation.
-
observability.Checkablefrom contracts.health.NewHandleracceptsobservability.Checkabledirectly fromcontracts/observability. Any starter (db-*,cache-*,storage-*) that implements the contracts interface plugs in without an adapter — nowebimport required by those starters. -
last_seenexcluded. Session tracking is an application-domain concern, not transport-level middleware. It requires knowing which entity to track and where to persist it. Developers who need it can write it in ~15 lines in their own wiring package. Will be revisited ifeinherjar/workerprovides a fire-and-forget primitive. -
UUID v7 for request IDs. Time-ordered UUIDs embed a millisecond-precision timestamp, enabling request IDs to sort chronologically in log aggregation systems. UUID v4 fallback ensures ID generation never fails.