16 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.7.1] — 2026-08-14
Patch — coordinated framework release (lockstep versioning). No changes to this module's own API.
Changed
- Bumped einherjar dependencies to v1.7.1. The change in this release is
spa-server's corrected MIME type for the PWA web app manifest (.webmanifest→application/manifest+json).
[1.7.0] — 2026-08-14
Minor — coordinated framework release (lockstep versioning). No API changes in this module.
Docs
- Generalized ADR-001 (request binding): removed the reference to a specific private consumer service and its internal ADR; the motivation is now stated in general terms. The framework's ADRs must be self-contained. No code change.
Changed
- Bumped
contracts,coreto v1.7.0.
[1.6.0] — 2026-08-13
Minor — request binding from path and query, not only the JSON body.
Added
httputil.Bind[Req, Res]andhttputil.BindEmpty[Req]— a fourth adapter family that fillsReqfrom the path, the query string and the body, each field declaring its source with a struct tag (path:/query:/json:), then validates the assembled struct once with the samevalid.Validator. The handler signature is identical toHandle/HandleEmpty;WithStatusand the full error-mapping pipeline are reused unchanged.- Conversion covers
string, the sized integer/unsigned/float types,bool, and any type whose pointer implementsencoding.TextUnmarshaler— souuid.UUIDandtime.Timebind with no special-casing and no new dependency inweb. - A conversion failure is
ErrInvalidInputnaming the parameter (400, never 500). default:applies only when a parameter is absent (a present-but-empty?q=is left as the zero value). Repeated query parameters bind to a slice; a comma inside a single value is not split. A bodilessGET/DELETEis not an error —BindEmptyretires theHandleEmptyempty-body (io.EOF) trap for routes keyed only by a path parameter.- The struct is reflected over once per type and cached. A field with more than one source
tag, an unsupported field type, or a
default:that is not a valid value for its field all panic at wiring — a mis-tagged struct fails the service at boot, not on a request.
- Conversion covers
Changed
HandlerFunc's doc comment no longer advertises itself for path/query parameters — those go throughBindnow; it remains the escape hatch for genuinely custom responses (streaming, file downloads, non-JSON).Handle,HandleNoBody,HandleEmptyandHandlerFuncare behaviourally unchanged.- Bumped
contracts,coreto v1.6.0.
[1.5.0] — 2026-08-09
Minor — configurable success status on the httputil handler adapters.
Added
httputil.WithStatus(code int) Optionand variadicopts ...OptiononHandle,HandleNoBodyandHandleEmpty. Override the success status — e.g.WithStatus(201)on a resource-creating POST,WithStatus(202)on an asyncHandleEmpty. Non-breaking: existing calls keep their defaults (200 / 200 / 204).
Changed
- Bumped
contracts,coreto v1.5.0.
Notes
WithStatusis success-only: the adapters own only the happy path, so the code must be 2xx. A non-2xx code panics at wiring (the service fails to boot) rather than emitting a wrong status at runtime. Error status stays separate — resolved from the returned xerror byError.
[1.4.0] — 2026-08-09
Minor — resolvable request IDs, plus a dependency refresh.
Added
mw.RequestIDFrom(resolve func(*http.Request) string)— the resolver sees the request, so a service can continue a correlation ID a client already sent (a distributed trace survives this boundary). The framework provides plumbing only: it does not read a header, choose a header name, or validate the value — that policy is the application's, because a value the framework accepts on a service's behalf may be one that service cannot store. Generation becomes the fallback branch of resolution rather than a separate mode.
Changed
mw.RequestID(generator func() string)is unchanged in signature and behaviour (always generates, ignores inbound); it is now expressed asRequestIDFromwith a request-ignoring resolver.- Refreshed dependencies (
go-chi/chi/v5v5.3.1,golang.org/x/timev0.15.0). - Bumped
contracts,coreto v1.4.0.
Notes
- An empty resolver result attaches no ID (header omitted, context carries none) rather than a silently-empty value; a resolver that can return "" is a caller error.
[1.3.0] — 2026-08-08
Minor release carrying a breaking API change to CORS configuration. The framework is
private with controlled consumers, so this ships in the 1.x line with a loud compile break
instead of a v2 module-path (/v2) migration.
Removed
-
⚠️ BREAKING:
web.Config.AllowedOriginsremoved. CORS origins now have a single home:server.Config.CORSOrigins(envEINHERJAR_SERVER_CORS_ORIGINS). The field was env-backed through v1.1.x and a code-only override in v1.2.0 — reading it after the env tag moved silently served no CORS. Removing it turns that runtime trap into a compile error.Migration: replace
cfg.Web.AllowedOriginswithcfg.Server.CORSOrigins, andweb.Config{AllowedOrigins: o}withweb.Config{Server: server.Config{CORSOrigins: o}}— or just letweb.NewreadEINHERJAR_SERVER_CORS_ORIGINS. The MCP flags any leftover reference (validate_snippetruleweb.allowedorigins-removed).
Changed
- Bumped
contracts,coreto v1.3.0.
[1.2.0] — 2026-08-08
Minor — CORS configuration moved to its rightful struct; web.New made safe-by-default.
Changed
CORSOriginsnow lives onserver.Config(env varEINHERJAR_SERVER_CORS_ORIGINS), the struct its name advertises — it previously loaded intoweb.Config.web.Config.AllowedOriginsremains as a code-only override (no env tag). Wiring viaweb.Newor the env var is unaffected.- Bumped
contracts,coreto v1.2.0.
Added
web.Newlogs a warning when no CORS origins are configured, instead of silently disabling CORS.- Package docs (
web,web/server) document when to useweb.Newvsserver.New, with compiling examples and the env-gated allow-all CORS convention.
[1.1.3] — 2026-08-08
Patch — CORS documentation discoverability.
Fixed
mw.CORSandCORSAllowAlldoc comments now document the"*"rejection (panic) and the env-gated CORS convention (local -> CORSAllowAll, elsemw.CORS(origins)), sosearch_symbolssurfaces it — previously the convention lived only in code comments and the wire example.
Changed
- Bumped
contracts,coreto v1.1.3.
[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.