Skip to content

Melody v3.14.0 - Stabilization Sweep, Hardened Security and Lifecycle Paths and the Last Feature Release

Choose a tag to compare

@drjele drjele released this 19 Oct 11:24
· 21 commits to main since this release
7ed1dd6

Added

  • cli: output.DecimalIntegerConfig is the base-ten IntegerConfig the standard integer flags and the framework's own command flags declare, for a command's own integer flag to declare through its Config.

  • validation: ConstraintLessThanErrorNotNumeric and ConstraintGreaterThanErrorNotNumeric code the bounds' not-numeric refusals.

  • http/middleware: (*FixedWindowLimiter).SetEvictOldestKeyAtCeiling and its sliding twin decide what a full table answers an unseen key: on, the key whose last request is the oldest gives up its bucket, so a flood of unseen keys costs an idle caller its count rather than every stranger its admission, and a ceiling lowered under a full table evicts the oldest keys at once; off, the default, the unseen key is denied as before.

  • messagebus: RegisterTransports registers a TransportsCloser beside the transports map, under ServiceTransportsCloser, resolved as a dependency of the map; (*ConsumeCommand).IsLongRunning has the application journal the unbounded default cache backend when a consumer is dispatched from the console, as for an http process, and a command of the application's own that runs until stopped declares the same method.

  • exception: PanicCause reads a recovered panic value as the cause of the error a recovery boundary fabricates in its place, a typed nil answering no cause, since its Error() would dereference a nil receiver; Logged answers an error that reports itself already logged, for a writer that filed its record, and IsAlreadyLogged reads the mark at the depth MarkLogged writes it.

  • openapi: Behavioural change: Registry.MarkServing records that the wiring phase is over; Application.Run calls it when it marks the configuration serving, and a Describe issued after it is refused at the door, naming the route.

  • http: DeleteCookieWithDomain(response, name, path, domain) writes the expiring counterpart of a cookie set with a Domain, which DeleteCookie writes host-only and the browser therefore never matches; a __Host- name drops the domain its prefix forbids.

  • http: ServerSentEventHub.ClearBackplaneWithContext(ctx) clears the backplane and waits for the broadcasts holding it under the caller's deadline, answering the context's error when the deadline ended the wait; the amqp backplane's CloseWithContext clears itself through it.

  • container: WithCloser[T](closer func(context.Context, T) error) closes a service through the closer, handed the teardown's remaining budget, in place of the Close and CloseWithContext doors of a value whose type the application does not own; the service keeps its type. It covers the built value, an evicted override, a value built after closing began and an override of the closer's type; another type closes through its own doors. RegisterOptions.Closer and RegisterOptions.CloserValueType carry it; ErrCloserTypeMismatch refuses a closer the provider's type is not assignable to, ErrScopedCloserUnsupported the option on both scoped registrations. The amqp connection service uses it.

  • container: WithoutTeardownReflection() keeps the armed teardown's walk out of a service's memory, for a service whose own goroutines rewrite a pointer field, which was a data race under go test -race at its filing and at ArmParallelTeardown: its value is not walked under its name, its type or an override of the name, and a walk reaching it records the edge without entering it. RegisterOptions gains SkipsTeardownReflection.

  • container: ContextCloser names, on the contract package, the door the teardown prefers on every value that carries it — CloseWithContext(context.Context) error, Close under a deadline the caller declares — asked by a type assertion, never a method of Container or a service contract; the container, the request scope, the application's close, the server-sent-event hub and the message bus ask for it by this one name

  • container: ArmParallelTeardown() error on the concrete container, reached by type assertion as IsClosed and CloseWithContext are, closes in one wave every service the dependency graph proves unrelated to the others in it, each with the whole remaining budget, so a component after a slow one is no longer reached with the deadline gone; off unless armed. Arm it after the wiring and before the run, the orderings written by a provider that resolves its collaborator or by WithTeardownDependency: an edge naming a service never registered is refused with ErrTeardownDependencyWasNeverRegistered, and two Close methods sharing an external state (a file, a table, a sql.Register name) only a declaration holds apart. ErrTeardownDependencyIsScoped is the cause ArmParallelTeardown, and any registration after it, answers for a declared dependency naming a scoped service

  • container: WithTeardownDependencyOfType[T]() declares a teardown ordering keyed by the collaborator's TYPE, beside the form keyed by its name, for a collaborator reached through GetByType or whose name belongs to a module the declaring code does not own; T and *T name one node. A registration that does not file its type may declare on its own Go type; only one that files the type it declares against is refused as declaring on itself.

  • container: container.ErrScopeClosed and container.ErrContainerClosed are the causes every closed-lifetime refusal carries — ten scope refusals, the lazy handle's and the container's eight — so errors.Is tells a request that ended from an application shutting down without matching the message.

  • container: container.WithTeardownDependency(serviceNames ...string) declares that a registration must be CLOSED BEFORE the services it names, without resolving them; an empty name and a self-reference are refused at registration with ErrTeardownDependencyNameIsRequired and ErrTeardownDependencyIsSelf, and RegisterScoped refuses it with ErrScopedTeardownDependencyUnsupported, a scope building its graph from its own resolutions; DeclareTeardownDependency(serviceName, dependencyNames...), on the concrete container by type assertion like ArmParallelTeardown, declares the edge after the registration with the rules of WithTeardownDependency, for an author who is not the one who knows the edge.

  • container: a service the container owns stays one instance for the process even when resolved through a request scope; Replacing() admits a scoped registration whose name or registered type a singleton already claims, not the reverse; ClosedWithScope() hands an installed override to the scope's teardown, through the new OverrideInstanceWithOptions and OverrideProtectedInstanceWithOptions; a scope closes what it built in dependency order, dependents before their dependencies, falling back to creation order, latest first, and reports a cycle as the container's teardown does; a collection gathered on a scope holds the scope's registrations and the container's, one gathered by a container provider only the container's.

  • container: IsClosed reports whether a Close already began tearing the container down; ServiceDescriptions on the built container answers what it can say WITHOUT running a provider

  • cache: cache.DeserializationError is the typed error a read answers for a stored payload that does not deserialize, with NewDeserializationError and IsDeserializationError as its doors; cache.NewManagerOwningBackend builds the manager whose Close cascades into the backend, the cascade NewManager no longer performs; (*Manager).NormalizeStoredValue answers the shape a value stored through this manager reads back as, one local serializer round-trip.

  • cache: RememberOption.WithContext ties a caller's wait to the request that opened it, and Context reads it back; it answers a COPY of the option carrying the context and leaves the receiver as it was, the zero value included, so a service keeps one configured option and derives a per-request one at every call, where written into the shared receiver, as the frozen majors' WithContext does, one request's context governed every concurrent wait on the option. A caller that discards the answered value keeps the option without a context.

  • cache: cache/contract.StoredValueNormalizer names on the contract package the door through which Remember learns the shape a stored value reads back as, NormalizeStoredValue(value any) (any, error), which Manager implements; it is asked of the Cache value Remember was handed, so a decorator over the manager implements it too, forwarding to the decorated manager, or the miss and the hit answer two shapes again; it was an unexported interface of the cache package.

  • session: session.ErrSessionDeleted is the cause the save refusal carries for a session deleted while the request holding it ran, and ErrSessionRotated names one refused because the id was rotated away, beside ErrSessionDeleted: a logout expires the browser cookie, a rotation moves the identity to the fresh id the rotating request hands the client

  • session: session.NewManagerWithClock(storage, ttl, tombstoneRetention, clock) reads every instant of the tombstone record from the given clock, and NewInMemoryStorageWithClock, NewFileStorageFromPathWithClock and NewFileStorageFromFileWithClock read every expiry instant, stored, compared and the sweep's tick, from an injected clock; the framework-wired default storage runs on the kernel's.

  • session: session.NewManagerWithTombstoneRetention builds a manager whose write-back refusal window the caller sizes, refusing a non-positive window with a panic naming the rule, session.TombstoneRetention being the default; session.NewManagerOwningStorage is the constructor whose Close ends the storage too, the cascade NewManager no longer performs.

  • logging: Behavioural change: NewReopenableFileWriter opens the journal behind a writer that can be told to open its path again, and the container-built logger arms it on SIGHUP in every process, the console commands included, so a postrotate SIGHUP rotates a long-running worker's journal too. SIGHUP no longer terminates a process whose journal is open, a console command whose terminal hung up included: stop one with SIGTERM or SIGINT.

  • logging: logging.ErrRotatedDescriptorNotClosed is the sentinel ReopenableFileWriter.Reopen joins to the one failure after a successful rotation; logging/contract.LevelReporter is the optional capability by which a Logger says whether a record at a level would survive its threshold, logging.LevelEnabled the door onto it; NewJsonLoggerWithClock stamps records from an injected clock, the kernel's for the container-built logger; Closed() on the json logger reports whether Close really closed the underlying writer, which NewRequestLogger's decorator forwards

  • logging: NewStandardErrorLogger adapts a melody logger to the *log.Logger the standard library asks for, so net/http's Server.ErrorLog or bun's diagnostic channel reaches the application's journal instead of stderr; NewProcessLogger is the console sibling of NewRequestLogger, the cli entry point decorating with it.

  • logging: RunShieldedStep runs one step under the exit handler's own budget and answers whether it finished, so the normal return of Run tears down under the panic path's shield; LogOnRecoverAndExitAfter logs the recovered value as LogOnRecoverAndExit does and runs a beforeExit hook between the record and the exit, under the budget its caller declares, a non-positive one taking the exit handler's own.

  • documentation: README.md carries one version table — each line's first release, latest tag, status and the event that ends its fixes — which SECURITY.md links to instead of a second, drifted one, with v4 planned for Q1 2027 and v3's own boundary; README.md and .documentation/UPGRADE.md date the v4 cut at Q1 2027 and say what it is for: adopt each replacement while still on v3 and the cut costs nothing. The frozen majors' support ends on the dates the version table gives, 2027-08-17 for v1 and 2027-09-08 for v2

  • validation: ValidatorLimits, DefaultValidatorLimits and NewValidatorWithLimits configure the bounds of one Validate answer: its entries (1,000 by default), the bytes of the path one entry spells (256) and the bytes of its paths and messages together (512 KiB); a validator registered over the framework's applies them to every request body, a zero field keeps its default and a negative one is refused.

  • validation: IsRuleWiringErrorCode reports whether a code names a mistake in the DECLARATION rather than the value — an unknown rule, an unreadable tag, a pattern that does not compile — which no client input can produce, ValidationErrors.HasRuleWiringError asks it of a collection and ValidationErrors.WithoutRuleWiringContext strips the internal context of those entries for the client; ValidationErrors implements json.Marshaler as the array it is, so a log record says what the response body says

  • application: two boot warnings name a resource melody supplied that nothing will ever reclaim; what net/http reports on its own reaches the application's journal; the comment on MiddlewarePriorityStatic describes where the static file server sits.

  • debug: debug.NewEventCommand takes a declaration channel, debug.DeferredListener entries naming what the SERVING process wires and this process does not, so debug:events run from a console answers "is access control wired?" with a declaration instead of an absence.

  • debug: the error-context walk stops at a depth bound; debug:container --build and debug:middleware --build are the doors that run providers and factories, and debug.MiddlewareDescriptionProvider and debug.MiddlewareBuildProvider are the two channels debug:middleware reads, the pipeline as selected and ordered and the chain built behind --build, which NewMiddlewareCommandWithProviders takes; debug:router reports priority and order, and under --verbose requirements, defaults and attributes. NewMiddlewareCommand, the one-provider door, keeps the two-column listing v3.13.0 rendered for it, index and middleware, since its provider answers no priority.

  • httpclient: HttpClientConfig.WithoutRedirects() *HttpClientConfig builds a client that hands a redirect back as the response it is, the 3xx status and its Location reaching the caller, and FollowsRedirects() bool reads the policy back: a following client re-sends a POST answered 301, 302 or 303 as a GET without its body, so a sink's success read off the redirect was another page's. The credential-stripping policy stays the default.

  • httpclient: HttpClient.Close releases the idle connections the client's own transport holds; RequestStreamWithContext is RequestStream bound to a context, whose cancellation ends the request and the body read, the only remedy for a stream a server never ends.

  • cli: cli.DispatchCommand(ctx, command, runtime, arguments, writer) parses one command line against a command's own flags and runs it with no banner, no scope close and no exit handling; the engine parses into fresh instances of the command's flags, so a command answering the same Flags() instances on every call is dispatched as any other, twice at once included, a declared Destination being the one pointer both write into; and under ValidateDefaults a declared default the flag's own validator refuses is refused at the dispatch as at registration.

  • cli: --format=json-pretty renders the envelope indented for a person, and output.IsJsonFormat answers for both json spellings wherever FormatJson was compared; NormalizeVerbosityArguments rewrites -v, -vv and -vvv into the verbosity level, the one door the console entry and DispatchCommand share.

  • event: event.RequiredListenerSkippedError is the type a dispatch returns when a listener ended it, by stopping propagation or failing, with a listener marked required through RequiredListenerRegistrar still behind it, and event/contract.RegisteredListener carries Required and MaySkipRequiredListeners, telling an armed fail-closed guarantee from an unarmed one

  • security: MarkInfrastructureFailure and IsInfrastructureFailure export the mark that tells a platform that could not answer from a credential that failed its checks: an application's TokenValidator, TokenEnricher or impersonation resolver returns its store outage through MarkInfrastructureFailure, and the bearer, internal-auth and switch-user sources file it at Error instead of the routine Info, a branch only the framework's own validators reached.

  • security: NewTwoFactorRejectedToken and the optional securitycontract.TwoFactorRejection door tell a second factor supplied and refused from a challenge not yet answered: TotpSecondFactorAuthenticator answers a wrong or replayed code, and a recovery code the store refused, with the rejected form (a store without TwoFactorRecoveryStore ignores the recovery header and answers the plain challenge), and (*TwoFactorPendingToken).SecondFactorRejected() reads it; both stay the pending token the application prompts from.

  • security: (*PathPrefixMatcher).Prefix() answers the raw prefix the firewall matcher compares with, and (*PathPrefixMatcher).String() describes it as path prefix "/admin", so CompiledFirewall.MatcherDescription() and SecurityContext.MatchedFirewallMatcher() name it where they answered the empty string.

  • security: HmacTokenSourceConfig.AcceptUntypedEnvelopes opens a migration window in which an internal-auth envelope carrying no typ is accepted, so a rolling upgrade does not refuse the peers not yet redeployed; default false, and a typ present and wrong is refused either way. Set it on the verifiers first, roll the signers, then clear it. The window is removed in v4

  • security: security/accesscontrol is where the access control vocabulary now lives: NewExactRule, NewSegmentPrefixRule, NewRawPrefixRule and NewRegexRule each name their reach; NewRule(path, matching, config) takes the mode as a value and refuses the zero value; attributes arrive in a RuleConfig struct, so a later dimension is a field on a call site that already compiles. security.AccessControlRule and security.AccessControl alias the moved types, so existing code keeps compiling

  • security: accesscontrol.Control.MatchRule(path) answers the rule that claims a path with its declaration index, resolved as Match resolves, without copying the rule list Rules() copies; the access control listener reads it.

  • security: a revocation stops being an enumeration: security/contract.RevocationEpochStore declares RevokeBefore(userIdentifier, deviceIdentifier, instant) and RevocationEpoch(...), and EpochRevocableTokenStore composes it with RevocableTokenStore, which is untouched, so a store written against an earlier release still compiles; both stores melody ships satisfy it.

  • security: securityconfig.(*Builder).BootWarnings() answers the hazards the last BuildAndCompile found in the declaration, each a securityconfig.BootWarning with a stable Name, a Message and a Context that names the configuration and never a secret; the application writes them into the configured journal once, at the http boot.

  • http: (*ServerSentEventWriter).WithWriteBudget(time.Duration) *ServerSentEventWriter re-arms the connection's write deadline before every frame, through a ResponseController reaching it through any wrapper, where net/http armed the server's WriteTimeout once from the request line and cut a longer stream silently: a stream that keeps writing lives, a client that stops reading is cut one budget after the socket stops buffering, zero keeps the server's deadline, and a writer the controller cannot reach writes every frame as before. A handler hands in the server's WriteTimeout, read off request.Context().Value(nethttp.ServerContextKey), and keeps a keepalive ticker.

  • http: ServerSentEventHub.SetLogger gives the hub the journal it never had; ServerSentEventHub.Close is Shutdown under the one name the framework's teardown recognises; ServerSentEventHub.IsClosed answers the difference a caller's range cannot see, and ServerSentEventSubscriber.Topic reports the topic a subscriber holds

  • http: FilterRouteManifestByZone narrows a manifest to one zone and answers (RouteManifest, error), the zone read as the command reads its --zone flag, trimmed: an empty zone answers the manifest whole and an undeclared one is refused naming the declared zones, where a misspelled zone answered an empty manifest in silence; RouteZones and IsRouteZone name the declared zones, for a command to list the accepted spellings in its refusal; RouteManifestEntry carries host, schemes, locales and priority.

  • http: SessionCookiePolicy.Secure takes a SessionCookieSecurePolicy: the default SessionCookieSecureFromScheme derives the attribute from the resolved scheme as before, SessionCookieSecureAlways forces it on behind a proxy that terminates TLS but is not trusted for X-Forwarded-Proto, which emitted every session cookie without Secure, and SessionCookieSecureNever is the explicit, discouraged opt-out.

  • http: a middleware reference is satisfiable when the registrations sharing the referenced name cover the referrer between them, and the reference-gating check weighs only what the group being built assembles; PrefersHtml is covered again for an html type that arrives alone in mixed case.

  • http: Behavioural change: RequestPathAsRouted(escapedPath string) string is the spelling of a request path the router reads — the escaped path split on the separators the client sent, each segment unescaped on its own, an encoded separator kept as %2F inside its segment — for every other consumer of the path to read the same: the kernel's canonical-path guard, the access-control matcher and the firewall path matcher read it now, where they read the decoded URL.Path

  • container, debug: TeardownPlanEntry carries Aliases, the other node keys of the same instance, and Cycle, set on a service ON a ring the drain could not open; debug:container answers a folded name with its node's entry, named by the node json key and printed service:a (also service:b), a type alias as type:*pkg.T, shows the row on a window on either name and reads cycle as a ring member's ordering, where an alias lost its teardown key and a ring rendered as two proved rows

  • container, debug: TeardownPlan() []containercontract.TeardownPlanEntry on the concrete container, reached by type assertion as IsClosed is, beside TeardownRunsInWaves() bool, is the read-only teardown plan, which debug:container renders as the TEARDOWN (SEQUENTIAL) / TEARDOWN (DEPENDENCY WAVES) block — per built service its wave, what it closes before and after, whether anything orders it — on the listing, the --build sweep and the single-service door, the json carrying a teardown object per built listed service, omitted where the plan lists none, so jq '.data.items[] | select(.teardown.ordering == "none")' names the services nothing orders

  • container, http, messagebus, logging: the teardown budget an operator declares REACHES the services released: CloseWithContext(ctx context.Context) error is the optional door the container's teardown prefers, falling back to Close() error, the Container contract untouched; http.ServerSentEventHub and messagebus.TransportsCloser carry it, as do the amqp transport and backplane, the bunorm registry and the audit storage. The deadline is SHARED, and the deadline record names the service that ate it and those left with none; a clean SIGINT under a stalled broker exits 1 naming them. MELODY_TEARDOWN_TIMEOUT=0 runs the teardown complete.

  • application, config, logging: MELODY_TEARDOWN_TIMEOUT (kernel.teardown_timeout) declares how long a clean shutdown may spend releasing what it holds, spent by the shield logging.RunShieldedStepWithin, where a package constant of ten seconds no deployment could change ruled it and a clean SIGINT exited 1 with one line on standard error. Zero means no deadline; a negative value fails the boot. It reaches the clean return from Run and the exit handler a panic or an exception.Exit takes, which, as the clean return does, reaches the brokers, pools and tracer providers a Close releases, keeping the package constant only when its caller declares none.

  • documentation: SECURITY.md teaches the access control rules through security/accesscontrol, names the four reaches beside the constructor that builds each, and points the type and method links at the package that declares them

  • config: four keys reach their consumer through config/contract.ExtendedHttpConfiguration, a sibling of HttpConfiguration, a configuration without it taking the default: MELODY_STATIC_EXCLUDED_PATHS (kernel.static.excluded_paths, read through config/contract.ExtendedHttpConfiguration.StaticExcludedPaths) names, comma separated, the path prefixes the built-in file server declines without touching the disk, none by default; MELODY_HTTP_SESSION_TTL (kernel.http.session_ttl, config/contract.ExtendedHttpConfiguration.SessionTtl) how long a stored session stays valid, default config.DefaultSessionTtl; MELODY_HTTP_SHUTDOWN_TIMEOUT (kernel.http.shutdown_timeout, config/contract.ExtendedHttpConfiguration.ShutdownTimeout) how long a stopping http server waits for the requests it admitted before cutting them, default config.DefaultHttpShutdownTimeout; MELODY_HTTP_SESSION_TOMBSTONE_RETENTION (kernel.http.session_tombstone_retention,
    config/contract.ExtendedHttpConfiguration.SessionTombstoneRetention) how long a deleted session id refuses a write-back, default config.DefaultSessionTombstoneRetention, five minutes.

  • event: event/contract.SubscriberRegistration identifies one installation of a subscriber, which event/contract.SubscriberRegistrar.AddSubscriberWithRegistration answers and RemoveSubscriberRegistration removes; both framework dispatchers implement the registrar.

  • http: RegenerateRequestSession rotates the session id of a request and republishes the rotated session in one call, the session-fixation defence a login handler needs; the handler writes the authenticated identity to the session it returns. It goes through the manager's session/contract.SessionRegenerator door, which the framework manager implements, and a manager without it is refused with an error, the request keeping its session. session.Manager.RegenerateSession stays the storage-level primitive: a fresh id, the values carried over, the old entry removed and the session passed in latched out of use, so a caller that does not publish the rotated one logs the client out cleanly.

  • container: a scope is a registrar in its own right, layered over its container: container/contract.ScopedRegistrar declares RegisterScoped and MustRegisterScoped, which the framework container and every scope implement and Application.RegisterScoped asserts on the kernel's service container. RegisterScoped on the container declares a service whose lifetime is one scope — an http request, a message delivery, a cron tick — built on its first resolution through a scope and closed with it, through the typed container.RegisterScoped[T], MustRegisterScoped[T], RegisterScopedType[T] and MustRegisterScopedType[T]; on a scope it adds one to that live scope.

  • http: (*http.Kernel).SetMethodPolicy installs the method policy the kernel reads on every request, whether HEAD falls back to the GET route and an unrouted OPTIONS is answered with the computed Allow header, and MethodPolicy moves onto the http contract; http/contract.MethodPolicySetter is the optional door a kernel is asserted for.

  • session: (*Session).Snapshot reads the values, the modified flag and the cleared flag under one lock acquisition, declared by session/contract.SnapshotSession; session.Snapshot, through which the response path and SaveSession read a session, uses it or reads All, IsModified and IsCleared.

  • wiring: melody:wiring:generate accepts --tags, the comma-separated build tags of the target binary, so a constructor gated on //go:build <tag> is scanned instead of silently dropped; wiring.ScanWithBuildTags takes them beside Scan's two parameters, Scan keeping its released signature.

  • internal: CopyAnyValue copies one value at the depth CopyAnyMap copies a whole map, so a caller holding a single any no longer has to wrap it in a map to get the same documented boundary

  • tooling: .dev/validate/documentation.sh checks the package documents against the code of every major, and now runs in the staged mode as well as the full one

  • security: JwtConfig.RevocationEpochSkew widens a revocation boundary by the worst clock skew between the node that mints and the node that revokes, and covers the imprecision of iat itself.

  • security: Behavioural change: security/contract.Claims carries IssuedAt and DeviceIdentifier.

  • security: NewJwtTokenValidatorWithRevocationEpoch makes a JSON web token revocable, which it has never been

  • http: SimpleRateLimitWithResolver, IpRateLimitWithResolver and UserRateLimitWithResolver take the client-ip resolver the convenience helpers could not reach.

  • http: static.FileServerConfig.SetAllowedDotPrefixList names the dot-prefixed first path elements the file server may retrieve.

  • cache: Manager.GetCounter reads a key written by Increment or Decrement.

  • config: a dollar in an .env value opens a reference only where godotenv says it does — upper case, digits and underscore.

  • openapi: a field the validator rejects for every value is advertised as not: {} rather than as an object-level contradiction.

  • application: ScopedServiceModule is the module hook for request-lifetime services, with RegisterScopedServices(registrar ScopedServiceRegistrar) running at boot beside RegisterServices.

  • wiring: //melody:scoped on a constructor emits it into a second generated function, <function>Scoped(registrar containercontract.ScopedRegistrar), through MustRegisterScoped/MustRegisterScopedType.

  • security: InMemoryTokenStore.WithRevocationEpochRetention bounds how long PurgeExpired keeps a published revocation boundary after its instant, mirroring the rueidis store's option of the same name.

  • security: every time-verifying component takes an injected clock — JwtConfig.Clock, HmacTokenSourceConfig.Clock, HmacEnvelopeSignerConfig.Clock, TotpSecondFactorAuthenticatorConfig.Clock and NewMemoryNonceGuardWithClock — defaulting to the system clock, the seam the token store and the rate limiter have always had.

  • application: ProcessContext — ServiceProcessContext, NewProcessContext, ProcessContextMustFromResolver, ProcessContextFromResolver — is the console counterpart of the http request context

  • http: Kernel.HasErrorHandler reports whether the application installed an error handler

  • http: Kernel.OpenRequestScopes counts the request scopes opened and not yet closed — one per request being served, hijacked connections included — and the http shutdown drains on it within the shutdown budget.

  • http: RequestContextMustFromResolver and RequestContextFromResolver resolve the current request's context — its id and start moment — from a request scope's resolver, the counterpart of application.ProcessContextFromResolver for the http process shape

  • http: RouteRegistry.SetBootCollisionRecorder arms the aggregated boot-collision channel with BootCollisionKindHttpRoute and BootCollisionKindHttpRouteName

  • bag: ParameterBag.AppendString adds one string under a name inside a single critical section, and All copies a stored string slice or string map instead of handing back the live value.

  • application: an http process whose MELODY_ENV no .env artifact sets names it once when it starts serving, at WARNING under bootWarning=application.environment.defaulted, since the kernel reads the missing name as dev and serves with the debug tooling on.

Changed

  • documentation: README.md, the major's own readme, CONTRIBUTING.md, SECURITY.md and the two GitHub templates say that v3.14.0 is the last feature release of v3, that the next features ship in v4 and that v3 keeps receiving security fixes and patch-level defect fixes, where they said every new feature lands on v3 until v4 ships.

  • security: Behavioural change: an access control rule path that does not begin with a slash is refused at declaration, by accesscontrol.NewExactRule, NewSegmentPrefixRule, NewRawPrefixRule, NewRule, the builder's Require and AllowAnonymous and the deprecated security constructors (access control rule path must begin with a slash): Require("admin", "ROLE_ADMIN") never matched the canonical request path, so its paths reached their handlers undecided. The empty raw prefix stays the fallback. A path with an empty, . or .. segment is refused too (access control rule path must be canonical), the declared spelling checked before its trailing slash folds, so /admin// is refused by the path as written; //admin or /x/../admin governed nothing either.

  • security: Behavioural change: a raw prefix rule declared with a trailing slash is refused at declaration, by accesscontrol.NewRawPrefixRule("/api/", …), the builder's Require and the deprecated security.NewAccessControlRule (access control raw prefix rule may not end with a slash): the trimmed slash let the rule claim /api-internal and outrank a regex rule for that tree. The reach is declared instead, a segment prefix rule or the raw prefix without the slash; / stays the root.

  • security: Behavioural change: accesscontrol.NewControl refuses a rule that carries no attribute, the zero Rule, which matched as the empty-prefix fallback and denied every path no other rule claimed.

  • security: Behavioural change: AuthenticatorTokenSource dispatches security.login.failure when the authenticator that supported the request answers no user, a wrong X-Api-Key among them, and the request still goes on anonymous; the event carries security credentials rejected, and a failing listener fails the request. A pending second factor is not announced, so a lockout listener does not lock every enrolled user on the way to the prompt; a second factor supplied and refused is, carrying security second factor rejected.

  • security: the firewall rules of a compiled firewall are checked through one Firewall built with it and the registry walks its firewalls in place, where each request copied the rule and firewall lists.

  • security: Behavioural change: a global access control declared without any firewall now enforces instead of being silently dropped.

  • security: Behavioural change: a zero-value FirewallOverrideConfiguration{} inherits the global access control as NewFirewallOverrideConfiguration() does, and gains the WithAccessControl, WithRoleHierarchy, WithAccessDecisionManager, WithMergeStrategy and WithInheritGlobalAccessControl setters, which read the constructor defaults when called on the zero value.

  • security: Behavioural change: the role voters refuse a token that reports roles while answering IsAuthenticated() false — the shape a half-logged-in token takes.

  • security: Behavioural change: AccessDecisionManager.DecideAll over an empty attribute list refuses instead of granting.

  • security: Behavioural change: an access control rule whose attribute list normalizes to empty is refused at construction instead of being accepted.

  • security: totp.OtpauthURI is now totp.OtpauthUri, the old spelling kept as a deprecated alias.

  • security: Behavioural change: PutWithTtl on the in-memory token store refuses a non-positive ttl instead of storing the token forever.

  • storage: Behavioural change: LocalStorage refuses, at every door, a key whose leaf is spelled exactly like one of its temp objects, .melody-storage-<sha256>.<16 hex>.tmp. Stored, such a key would be swept by a later Put of the key whose digest it carries.

  • validation: Behavioural change: exceeding the nesting-depth cap is reported as a validation error (nestingDepthExceeded) only when the truncated subtree could carry a validate tag, so nesting past the cap no longer bypasses validation while tag-free client json is accepted up to the cap as before.

  • logging: RunShieldedStep, RunShieldedStepWithin and LogOnRecoverAndExitAfter hand the step a context.Context carrying the deadline the shield holds it to, and the exit handler takes that budget as an argument, the budget spent in two halves: the step's deadline and the headroom in which a step that honoured it says what it could not release, where a step returning exactly on its budget was reported abandoned; the json logger takes its timestamp inside the write mutex with the encoding, so the stamps' order is the writes', and allocates less per record (BenchmarkJsonLogger_Log), the bytes of every record unchanged.

  • config: Behavioural change: the comment cut matches godotenv's own, and a value that is EMPTY before a trailing comment reads as empty: APP_SECRET= # fill this in, the placeholder line of a copied .env.example, loaded # fill this in as the secret and a $WORD inside it failed the boot as an undefined reference; KEY=#glued stays data and a non-empty value keeps godotenv's countback.

  • documentation: the example application inside this major keeps no changelog: .example/README.md is the whole of its documentation, a database left in an older shape is answered by example:db:reset, CONTRIBUTING.md says a change to an example is filed nowhere, and the citation band's corpus follows the document that went

  • documentation: the version table names the date each line is supported until and the rule those dates come from, where v3's row read "v5 is released": the current line receives features, security fixes and deprecations, and the line a new major replaces receives security and patch-level defect fixes for eighteen more months from the successor's release. v3 carries no end date, its clock starting the day v4 ships; v1 is supported through 2027-08-17, v2 through 2027-09-08

  • cli: Behavioural change: --order=desc reverses the listing in debug:router, debug:events, debug:parameters, debug:middleware and debug:container, applied before --limit/--offset, so a descending window returns the end of the list

  • openapi: the schema mirror advertises as an unsatisfiable field what the validator now refuses, where it published a satisfiable bound the server never enforces: an empty regex pattern (regex=), which compiled "" into a pattern matching every string; a numeric bound not an integer in its entirety (parseBoundStrict, the twin of the validator's parseIntStrict), so max=99.5 read as 99 and greaterThan=5x; a negative min/max, null included, clamped to minLength: 0 or keeping its null; the string constraints (min, max, regex, email, alpha, numeric, alphanumeric, notBlank) on a non-string shape; a tag that parses to no rule (validate:",").

  • serializer: Behavioural change: a range carrying q=0 refuses the types it covers rather than being ignored, and ErrNotAcceptable is answered only when every registered type is refused.

  • serializer: Behavioural change: the accept header is read on the strict grammar the http readers share, internal.ParseQualityValue for q and internal.SplitOutsideQuotes for members and parameters, so q=1e-1 and q=1.5 lose their member

  • serializer: Behavioural change: NewSerializerManager refuses a map in which two spellings collapse onto one normalized key, naming the normalized key and both spellings in sorted order.

  • bag, http: Behavioural change: a request parameter that appeared once is stored as the string it is, a repeated one stays a string slice, and reading the repeated one as a single string answers its first value, as Request.Input and url.Values.Get do, where an earlier form of the branch panicked into a 500 through StringOrDefault/HasNonEmptyString; the whole list is read with StringSlice/StringAt

  • session: the framework-wired session manager reads the KERNEL's clock, as the framework-wired session storage does, so a fixed-clock deployment moves the tombstone record and the entry expiry together; a manager the application wires itself defaults to the system clock, the reading time.Since used, in every constructor but the new clock-taking one.

  • translation: Behavioural change: a plain placeholder whose parameter is ABSENT renders as the visible placeholder itself instead of as an empty string, and so do a plural and a select argument whose parameter is absent, where {count, plural, …} rendered its other branch with the number deleted ( items) and {gender, select, …} its other branch; a parameter present with a nil value renders as before.

  • container: FromResolver and FromResolverByType wrap a foreign error, from a resolver implemented outside the package or a provider's raw driver error, under the title service resolution failed in resolver with the error as the cause, where the title read service not registered in resolver for a registered service whose provider failed; a melody error travels out whole, and the service name or type is kept in the context.

  • container: Behavioural change: the teardown breaks a tie the dependency graph leaves open on creation order, latest first, not on the node key descending.

  • container: closing has two states, and a service's own Close may still resolve; Has and HasType answer under the suspension Get enforces; Scope.RegisterScoped refuses a closed container, as the other two registration doors do; a scope override answers the type-keyed resolutions of every type its name is registered under, as the container-level override does.

  • container: Behavioural change: a closed container refuses new registrations and new overrides the way its scoped registrar always has.

  • container: Behavioural change: an override must fit every type its name is registered under.

  • container: Behavioural change: a typed-nil provider function is refused where it is registered, at the one gate all three registration paths go through.

  • container: Behavioural change: two DIFFERENT types that share one identity key refuse to coexist, at the container door and the scoped one alike.

  • container: Behavioural change: the protected service. namespace holds at both lifetimes.

  • container: Behavioural change: the override assignability guard judges the value the way the readers will, on the scope as well as on the container.

  • container: Behavioural change: a lazy handle follows the scope of the resolver it was built over instead of memoizing forever.

  • cli: cli.Root, built by cli.NewRoot over cli.NewCommandContext, is a command tree that owns its command list: Register appends a command as cli.Register does on a root command, SetWriter and SetErrorWriter reach the tree and every command registered on it, whenever it was registered, and Run and CommandNames dispatch and list. The inert exit handler of NewCommandContext is the tree's, with no door to remove it.

  • config, bag: Behavioural change: the string grammar of the float accessors is plain decimal — an optional sign, digits and at most one decimal point.

  • bag: Behavioural change: ParameterBag.Get copies at the depth All copies — a stored []string or map[string]string is answered as the caller's own.

  • cache: NewDefaultRememberOption keeps the released detached flight; under the opt-in WithCancelable(true) the last waiter leaving cancels the flight and the next caller leads a fresh one, so a hung callback no longer owns its key for the process's life, but a wait timeout shorter than the callback never stores: pair a bounded wait on a slow callback with the default, or give the callback its own deadline.

  • cache: Behavioural change: NewManager builds a manager that does NOT own its backend, so Close leaves the backend open; NewManagerOwningBackend keeps the cascade.

  • cache: Behavioural change: the key grammar is stated on the cachecontract.Backend contract — non-empty, no spaces, no newlines, at most 1024 bytes — and the in-memory backend enforces it with the refusals the redis backend always answered.

  • cache: Behavioural change: the in-memory backend refuses four degenerate inputs it used to absorb.

  • cache: Behavioural change: an existing counter payload is parsed against the redis integer grammar rather than Go's lenient one — an optional minus, a first digit of 1-9 unless the whole number is the single digit 0, and nothing else.

  • cache: Behavioural change: Remember coalesces concurrent callers only for pointer-kind Cache implementations, whose address tells two instances apart.

  • cache: Behavioural change: the zero-value &RememberOption{} reads as NewDefaultRememberOption().

  • cache: Behavioural change: a waitTimeout of zero means no WAITING rather than no answer: a result the in-flight call has already memoized is taken without blocking, and only a flight still in the air answers with the timeout.

  • cache: Behavioural change: Remember answers one shape on the miss and on every hit, the computing call returning the value passed through the manager's serializer round-trip.

  • validation: Behavioural change: regex, email, alpha, alphanumeric and numeric refuse a value that is not a string instead of passing it in silence, which is the symmetry the numeric constraints already had.

  • validation: Behavioural change: minLength, maxLength and notBlank measure a string rather than a Go rendering, and refuse a value of any other type.

  • validation: Behavioural change: a rule that names a parameterized constraint WITHOUT parameters fails closed.

  • validation: Behavioural change: regex= with an empty pattern is refused at construction.

  • validation: Behavioural change: a negative bound is refused where it is written — at NewMinLength/NewMaxLength by panic, and at the tag door by a parse refusal, because a tag is data a request path reads.

  • validation: Behavioural change: a numeric constraint parameter must be an integer in its entirety.

  • validation: Behavioural change: a tag that survives the empty/skip-marker guard and then parses to no rule at all — a bare comma, for instance — is a syntax error rather than a request to validate nothing.

  • openapi: Behavioural change: the schema mirror follows the repaired validator, so the generated document changes for every tag class the validation repairs above touch — in the restrictive direction, never the reverse.

  • logging: Behavioural change: the json logger weighs a record whose level is not one of the five known ones as an error instead of inheriting the default lowest priority.

  • logging: Behavioural change: the json logger's timestamp carries nanosecond precision.

  • messagebus: the failure a transport whose Close panicked is reported under carries the stack of the panic, panicStack, as the container's close containment keeps it.

  • messagebus: Breaking: the Transport contract's Close takes no runtime and answers only the error — Close() error — and a userland transport deletes the parameter: the container's teardown recognizes Close() error alone, so the old Close(runtime) error was never called and every deploy tore its broker connection down abruptly.

  • messagebus: Behavioural change: InMemoryTransport.Close closes the delivery channel Receive handed out, so a consumer ranging over it sees the end of stream, where the channel stayed open forever and a consumer without its own cancellation blocked on Close; no send is ever selected onto the closed channel.

  • messagebus: Behavioural change: RouteType refuses a nil or typed-nil transport at registration, in the framed form RegisterTransports uses for a nil map entry, where the first message of its type panicked on the dispatch path.

  • messagebus: Behavioural change: a consumed message whose type has no registered handler fails the dispatch instead of being Acked with a warning, so a forgotten registration, or the pointer-vs-value keying trap, no longer drains a queue past the retry and dead-letter machinery; HandleOptions.AllowMissingHandler opts into the pass-through and RequireHandler stays, deprecated, reading as the default.

  • debug: NewMiddlewareCommand keeps its released one-provider form over MiddlewareProvider, and the description-and-build form is NewMiddlewareCommandWithProviders.

  • event: Behavioural change: AddSubscriber refuses a second installation of one subscriber pointer through itself with "event subscriber is already registered", as v2.13.0 does, since RemoveSubscriber would take both zero-size twins down; AddSubscriberWithRegistration installs a subscriber twice, or a value subscriber. A subscriber installed through AddSubscriber stays reachable through the dispatcher while installed, so one the application dropped is not collected and its address not handed to a fresh value the dispatcher would refuse as registered.

  • event: NewEventDispatcherAdapter keeps its released two-parameter signature, its clock refused when nil, typed nil included, and otherwise unused.

  • event: Behavioural change: a dispatch that ended early with a listener marked required behind it returns RequiredListenerSkippedError instead of completing, or instead of returning only the ending listener's own failure.

  • event: the propagation test opens the listener iteration instead of closing it.

  • event: Behavioural change: marking a listener through MarkListenerRequired or MarkListenerMaySkipRequiredListeners panics on a registration the dispatcher does not hold, where it used to be a documented silent no-op

  • event: Behavioural change: AddSubscriber validates every subscribed event before it registers a single listener, and refuses a subscriber that declares no events as well as an event name mapped to an empty list.

  • debug: Behavioural change: under --verbose the json document of debug:events carries data.{events, listeners}, where it carried the event list alone.

  • config: Behavioural change: a bare integer is refused where a duration is expected.

  • config: Behavioural change: Float64 refuses NaN and the infinities on every branch.

  • config: a typed-nil map[string]string reads as the absence it is.

  • config: Behavioural change: IntWithDefault answers the default only for a parameter that is ABSENT.

  • config: a .env line is preprocessed byte by byte rather than through runes.

  • config: Behavioural change: a ${...} reference in a .env value whose closing brace arrived over a name outside the key grammar is refused instead of surviving as literal text.

  • config: Behavioural change: an unterminated %env( and a %name% reference that nothing closed are both refused at boot.

  • config: Behavioural change: RegisterRuntime refuses a blank name and one carrying leading or trailing whitespace.

  • http: Behavioural change: NewRequirements refuses what it dropped, an empty parameter name, an empty pattern and a parameter declared twice, each named in the panic, keeping its value-variadic signature.

  • http: JsonHandlerFailureResponder, set through WithJsonHandlerFailureResponder, is handed the failure beside the status and the message; JsonHandlerErrorResponder keeps its four-parameter form.

  • http: Behavioural change: a request path that names one parameter twice — /orgs/:id/members/:id — or carries a nameless : is refused at registration.

  • http: Behavioural change: a route zone that is not one of the RouteZone* values is refused, at ExposedRouteAttributes and at registration, as are an exposed route with no name and an expose attribute that is not a bool.

  • http: Behavioural change: a trusted-proxy entry that parses as neither a CIDR prefix nor an address is refused where the list is ingested, Kernel.SetForwardedHeadersPolicy and NewForwardedClientIpResolver alike.

  • http: Behavioural change: route introspection publishes the requirement the caller DECLARED rather than the compiled form.

  • http: Behavioural change: JsonHandler reads the request body through the same door Request.BindJson is.

  • http: Behavioural change: every error the framework renders for a request — the exception listener's and the kernel's own 404 and 405 — goes through one door, negotiated against Accept, its json body carrying the request id beside the released error and time; JsonErrorResponse, which the security entry points answer through, keeps the {error, time} body unnegotiated, the request id on X-Request-Id.

  • http: Behavioural change: the public half of a validation failure's context travels under the key validationErrors

  • http: Behavioural change: JsonHandler rejects a literal null request body with 400 when its request type is a pointer, a map, a slice, an interface, a func or a channel, the kinds a null decodes to nil.

  • http: Behavioural change: a route pattern whose optional parameter is not its final segment is refused at registration, unless the parameter carries a non-empty route or group default, instead of being accepted and half-served.

  • http: the response path saves and advertises the session published under RequestAttributeSession when the response is written, not the one captured before routing; a header key the response names is owned by the response; Router.Match hands out a deep copy of the winning route's attributes instead of the registry's own map; the response the terminate event and the access log report is the one the client received.

  • http: TokenBucketLimiter is now FixedWindowLimiter, the old type name and both constructors kept as deprecated aliases.

  • http: Behavioural change: the request attributes the kernel owns are published after the route attributes, so a route attribute no longer replaces the session object or the resolved scheme, which is _scheme; RequestAttributeSession keeps its released value session.

  • http: Behavioural change: a urlencoded body whose read or parse failed is refused before the handler, 413 when the size limit stopped the read and 400 for a body the client broke, as the json binding refuses it.

  • http: Behavioural change: a session-storage outage on the save path answers 500 rather than the handler's response, the session cookie suppressed either way.

  • http: Behavioural change: two routes identical on everything the matcher discriminates — pattern, methods, host, schemes, locales, requirements and priority — are refused at registration, the later one never dispatchable.

  • http: Behavioural change: a handler-returned failure files one record at the level it deserves — a deliberate 4xx at warning, a client's own cancellation named as such at warning, everything else at error — and marks the error, so the exception listener attaches its request coordinates instead of filing the failure again.

  • http: a response status outside net/http's [100, 999] is refused by name on both write paths, the public WriteToHttpResponseWriter answering the error its signature promises before anything is written, the internal path logging and answering the rendered 500, where WriteHeader panicked; AllowedMethods applies the matcher's locale filter through the shared matchesLocale, so the methods announced for a path no longer name locale-scoped routes it cannot reach.

  • http: Behavioural change: the kernel's configuration doors and the router's registration doors are BOOT-ONLY and refuse rather than corrupt.

  • http: Behavioural change: a route is matched on the path as the client SPELLED it — URL.RawPath while it still unescapes to URL.Path, URL.EscapedPath when no RawPath was kept, and a RawPath that no longer unescapes to URL.Path refused — each segment unescaped after the split.

  • http: Behavioural change: matchesHost compares host names case-insensitively, the port significant only when the route declared one.

  • http: Behavioural change: route defaults are merged BEFORE the locale gate reads them, in Match and in AllowedMethods alike.

  • logging: Behavioural change: LogOnRecover only logs.

  • config: Behavioural change: Resolve() reports an error once the application is serving.

  • config: Behavioural change: a positive session ttl below one second fails the boot.

  • httpclient: Behavioural change: the client sets MaxIdleConnsPerHost on its transport, exposed as TransportConfig.MaxIdleConnsPerHost and defaulting to MaxIdleConns (100), following a positive override of it unless set itself, where net/http fell back to two per host; a caller relying on the two-connection ceiling sets MaxIdleConnsPerHost explicitly.

  • httpclient: Behavioural change: the response body cap bounds RequestStream too, the inherited default of ten mebibytes included

  • httpclient: Behavioural change: buildUrl resolves the target against the base url by RFC 3986 reference resolution, the rule Symfony and Guzzle implement, instead of joining the base as a prefix: a base url whose path lacks its trailing slash panics at construction, and an absolute-path target under a base with a path names a different resource than the prefix join did.

  • httpclient: TransportConfig keeps its released value fields, a non-positive one taking the default, MaxIdleConnsPerHost among them.

  • httpclient: Behavioural change: header maps are canonicalized at the door and two spellings that collapse onto one header are refused by name, as the serializer refuses its mime keys: the constructor panics at the wiring, and SetHeaders, a request-time door, writes nothing from a colliding map and the request fails with request option refused naming the option's index, where a panic bypassed the caller's handling.

  • httpclient: Behavioural change: RequestOptions.Headers and Query hand out copies, and the contract says so: the setters are the one door that writes.

  • validation: Behavioural change: a nil pointer embed is validated as "nothing was supplied" rather than "nothing to validate", so the constraints on its promoted fields run against their zero values exactly as a value embed's already did.

  • cli: Behavioural change: in json mode a command's standard output is the rendered document and nothing else — the ansi start/finish banner that cli.Register wraps around every registered command is suppressed, and --format=json implies --no-color.

  • cli: Behavioural change: a command whose envelope reports an error exits non-zero.

  • cli: Behavioural change: --format and --order reject an unrecognised value instead of silently substituting the default.

  • rate limit: Behavioural change: the default key extractor now keys on the client IP alone instead of ip:path. Both limiters also bound how many distinct keys they track (SetMaxKeys, default 1,000,000): once the map is full and an idle-entry prune frees nothing, a request under an unseen key is denied rather than growing the map without bound.

  • cli: Behavioural change: --format=json writes one document on one line, terminated by a newline, instead of an indented block.

  • cli: Behavioural change: text of server or client origin reaching the terminal is escaped rather than obeyed.

  • mailer: Behavioural change: configured smtp credentials fail closed when the server does not advertise AUTH, whatever RequireAuth says.

  • debug: Behavioural change: debug:container and debug:middleware describe by default and build only under --build.

  • debug: Behavioural change: the --build sweep reports its failures on the envelope, so app debug:container --build --format=json || exit 1 fails a deployment gate the way the single-service door always has

  • debug: debug:version reads the version rows off the envelope meta, where NewMeta has already applied the fallback chain — the command's explicit value, then the process-wide declaration made through output.SetApplicationVersion, then nothing.

  • serializer: Behavioural change: a manager deliberately configured without application/json answers an empty or unmatched accept header with its first configured serializer in lexical mime order, where it refused with no default serializer configured and the result handler served hardcoded json, the representation the configuration had excluded.

  • example: Behavioural change: the object storage reads S3_INSECURE (true dials plain http) in place of S3_SECURE; a .env that carried S3_SECURE is read as secure until the key is renamed.

  • example: the files a generator writes are git-ignored rather than committed, as the bundle built beside them is: the service wiring generated/wiring_gen.go, which go generate ./generated writes through a generator of the example's own under wiring/generate, since melody:wiring:generate is compiled into the application that imports the file, the bind set moving to the wiring package it shares with config; and the frontend route manifest assets/routes.json, which only the usage sample assets/routes-usage.ts reads, npm run typecheck generating it first, the pages receiving the manifest from the server. .dev/generate.sh writes the wiring at the dev container's start and before every gate; a bare host run writes the wiring with go generate ./generated before go run ..

Deprecated

  • cli: --fields and --sort, output.Option.Fields and SortKey, output.Flags.Fields and SortKey, output.FlagNameFields, output.FlagNameSortKey and output.SplitFields are deprecated: the two flags are declared and parsed as at v3.13.0, and no printer reads them; they are withdrawn in v4.
  • security: the four access control rule constructors are superseded by security/accesscontrol, whose constructors name their reach and take a config struct: NewAccessControlRule by accesscontrol.NewRawPrefixRule, NewAccessControlRuleWithSegmentPrefix by NewSegmentPrefixRule, NewAccessControlExactRule by NewExactRule, NewAccessControlRegexRule by NewRegexRule. All four keep working and forward to their replacement, so they refuse what it refuses (a trailing slash on a raw prefix, an unanchored PUBLIC_ACCESS regex); they are removed in v4, and moving to the replacement on v3 carries no change across that cut

Removed

  • application: Breaking: HttpTimeoutConfiguration and HttpShutdownConfiguration are removed, and only code naming either type stops compiling: no application could supply one, since the configuration NewApplication builds implemented neither; the shutdown timeout is read from MELODY_HTTP_SHUTDOWN_TIMEOUT.

  • cli: the dead unexported error presenter is gone, an error-printing path nothing called since the envelope renderer became the one presentation

  • session: burying a tombstone prunes only the lapsed burials, in the order they happened, instead of sweeping the whole record on every call; the in-memory storage sweeps expired sessions in chunks, releasing the lock between them and reading each entry again under the chunk's lock

  • config: the unexported loadRequiredDotEnvFile is gone, a required-file loader nothing called on any major.

  • session: Behavioural change: NewManager does not close the storage it was handed.

  • session: Behavioural change: Manager.SaveSession and Manager.DeleteSession hold a lock striped by the session id across the storage call rather than one lock for the whole manager.

  • http: a session another request ended while this one was running is recorded at warning, under its own name, and the response path expires the browser cookie and serves the handler's response unchanged.

Fixed

  • cli: a typed nil stream given to Root.SetWriter or Root.SetErrorWriter, or set on the root command cli.Register copies from, is handed to the engine as the nil it defaults, so the command writes to the process's own stream where its first write panicked.

  • documentation: NewDefaultRememberOption's comment says a callback that never returns pins its key for the life of the process under the default, every later caller parking behind it, and that WithWaitTimeout and WithContext bound a caller's wait, not the computation — the callback's own deadline or WithCancelable(true) frees the key — where it listed the two beside the deadline as what a hanging callback wants.

  • documentation: SECURITY.md states that a path no access control rule claims reaches its handler undecided and names the fallback refusing it (the Control.Match GoDoc said the caller decides), that an impersonation request lacking the header, the authentication or the switch role keeps the caller's token while a resolver error or unusable target answers an anonymous one, and, with the GoDoc of NewJwtTokenValidatorWithRevocationEpoch, that it sets RejectFutureIssuedAt whatever the config says.

  • documentation: the block's markers follow one rule — behavioural for what an application running v3.13.0 observes, breaking for a compile break, neither for a repair of what the released contract already called wrong — and the 21 **Behavioural change** markers the 08-28 compression deleted from the [Unreleased] entries where someone must act are restored: the boot refusals of a route or module from a boot hook, of RegisterConfiguration under any name but logging, of a scoped service. registration and of a reused RouteOptions; the wiring generator on a dead --strict exclude and a mistyped //melody: directive; debug:container --build failing a gate, --limit and --offset; a shared ParameterizedConstraint, a concurrent session Storage, a lazy handle over a closed scope, MaxIdleConnsPerHost following MaxIdleConns, a duplicated flag name, a mismatched table row; the json logger's unknown level, the handler failure record, nesting past the depth cap, an
    opaque token without an issue instant, the OpenAPI document, an absent translation parameter. NewAccessControlRule refusing PUBLIC_ACCESS and LogOnRecoverAndExitAfter gain entries, buildUrl is behavioural not breaking, MELODY_HTTP_SESSION_TTL unmarked, and the rueidis, bunorm and amqp duplicates removed. UPGRADE.md gains a section for every marked core and integration change that had none (among them Unlimited on the mysql and pgsql zero fields), names the manifest's --out anchoring and the undefined ${X} refusal, and drops two sections no released integration had.

  • documentation: the README's minimal sample writes a one-line .env, and it and VERSION.md build one main package, since go build -o app ./... refuses several; the README states the frozen majors' back-port rule, any patch-sized defect fix plus security fixes through the dates of its version table; UPGRADE.md covers MELODY_TEARDOWN_TIMEOUT, the latching Clear, JsonHandler's null refusal, the padded path, a lazy handle's Closed or IsClosed, SetBackplane, a q past three decimals (as SERIALIZER.md says), the registry teardown budget and two quoted refusals; the example's README names its scheduled commands, product:list's arguments, its directories, gates, containers and ports; APPLICATION.md's module example takes containercontract.Resolver where func(serviceLocator any) was refused at boot, and says RegisterConfiguration takes one name; WIRING.md uses RegisterCliCommand, not an undeclared RegisterCommand; ROADMAP.md drops three shipped items;
    DOCUMENTATION.md carries
    five missing passages (among them .github/ISSUE_TEMPLATE/ and the Exported API exception) and names its README.md the module root, which ../README.md resolves to.

  • cli: Behavioural change: a flag spelling declared twice, an empty alias, a spelling of the help flag (help, h) and a nil flag are refused by name at registration, through cli.Register and Root.Register, and at every cli.DispatchCommand, with cli flag spelling declared twice, cli flag alias is empty or cli flag may not be nil, where the first declaration won in silence and a flag spelled h ran the command for -h.

  • cli: Behavioural change: a flag that sets ValidateDefaults and whose declared default its own Validator refuses is refused by name at registration, through cli.Register and Root.Register, and at every cli.DispatchCommand, with cli flag default value is refused by the flag's own validator, where the engine refused every run of the command without naming the flag; a flag that leaves ValidateDefaults false is not checked, as the engine validates only the values it parses.

  • cli: Behavioural change: --verbosity, --limit, --offset and --table-width parse in base ten, where --limit=010 read as eight, --limit=08 was refused and --limit=0x10 read as sixteen; a base prefix is refused as syntax; the framework's own command flags — melody:messagebus:consume --limit and --concurrency — parse in base ten the same, and a command's own integer flag declares its base through its Config, output.DecimalIntegerConfig or another.

  • cli: Behavioural change: cli.NewCommandContext installs an inert exit handler, so a returned error, an exit-coded one included, reaches the application's exit path rather than os.Exit inside the engine's Run; a caller wanting the engine's default sets ExitErrHandler back.

  • cli: Behavioural change: output.MergeFlags refuses a duplicated flag name at the line that declares it, and refuses a nil flag.

  • cli: Behavioural change: TableBlockBuilder.AddRow refuses a row whose cell count disagrees with the block's declared columns, naming the block and both counts.

  • cli: Behavioural change: the table format prints the warnings under --quiet too, and renders the envelope error whole — message, code, details and cause.

  • cli: Behavioural change: the [error] line of a failing command goes to the command's ErrWriter, standard error by default, in every format, where it ended a redirected document and --format=json wrote none.

  • example: Behavioural change: without AMQP_DSN /messagebus/dispatch and /outbox/relay answer 503 naming the key and melody:outbox:relay refuses naming it, where they published into a process-local queue nothing read; the outbox keeps its rows until a broker is configured, and messagebus:dispatch keeps its in-process round trip.

  • example: Behavioural change: the history door answers, beside the archive, the current reading the scheduled refresh cached, or one taken on a cold cache; a reading is cached for the refresh interval, at least the schedule's hour and five minutes, where it lapsed unread after five minutes.

  • example: Behavioural change: the catalogue database is opened at its first use rather than at boot, as the archive is, so a process that never reads it, one serving only /health included, opens no connection; a declared catalogue that cannot be opened is refused, named with its location, by the first request or command reading it, where the boot refused it.

  • example: Behavioural change: the example's one schema migration is amended, so a volume built by an earlier checkout is refused by its fingerprint until example:db:reset --force rebuilds it; it holds the currency code unique, a product's currency and category by foreign keys ON DELETE RESTRICT, the username folded in Go in a username_normalized column with its unique key (in place of LOWER(username)), an identifier sequence and a per-account session index.

  • example: Behavioural change: a currency's code is its identity: a second currency under a taken code or a rename onto one, and the delete of a currency a product is priced in, are refused with 409, and a product write naming a missing category or currency is a 400, where they stored a wrong ?currency= conversion or answered 500; CategoryService.DeleteById refuses a category a product sits in with ErrCategoryInUse; the check and the write share the catalogue lock, and the write doors validate the trimmed spelling they store.

  • http: Behavioural change: SetCookie panics on a cookie name that is not a valid token, as on an empty one, where the standard library wrote an empty Set-Cookie; a SameSite=None session cookie is written Secure under SessionCookieSecureFromScheme whatever the scheme, since browsers drop a None cookie without it, SessionCookieSecureNever still honoured; a session response whose Cache-Control leaves a quote open is answered Cache-Control: no-store, private; the compression middleware reads level 0, NoCompression, as the default and closes a stalled source when the client leaves, without rewriting the caller's configuration; the static file server names a refused open by its cause and a fifo, a socket or a device by its mode, where both were journaled as resolving outside the served directory; the GoDoc of DeleteCookie says it deletes a host-only cookie, that of RateLimitConfig that the middleware reads it on every request, and that of NewOptions and of the
    content tag that the bytes of a filesystem without modification times must not change while it serves.

  • http: Behavioural change: a catch-all keeps an encoded separator inside the segment the client put it in, so /files/a%2Fb/c and /files/a/b/c no longer bind the same a/b/c; only the separator is re-escaped, and a literal %2F sent as %252F binds as an encoded separator.

  • http: Behavioural change: melody:routes:manifest mirrors the openapi generate command in the four places that decide whether its artifact is trustworthy, none of which it did while its own comment said it mirrored that command.

  • http: Behavioural change: a middleware may only reference, through before or after, a middleware enabled in at least every environment it is itself enabled in.

  • http: Behavioural change: a RouteGroup no longer writes its name prefix and its merged requirements and defaults back into the caller's RouteOptions.

  • http: Behavioural change: equal-priority middlewares now run in the order the application registered them.

  • http: Behavioural change: Kernel.SetSessionCookiePolicy no longer drops the SameSite=Lax default when the caller names only Path or Domain.

  • http: Behavioural change: a response that carries the session cookie is kept out of shared caches — public is dropped and private added unless it already says private or no-store — since a shared cache replaying it handed another client the first one's session id; the expiring cookie of a cleared or deleted session is kept out the same way, and the three doors that write the session cookie share one spelling of it.

  • http: Behavioural change: a route pattern written in a parameter syntax the router does not speak is refused at registration.

  • http: Behavioural change: a route whose locale declaration contradicts its pattern is refused at registration, in both directions.

  • openapi: Behavioural change: a relative --out is anchored at the project directory, exactly as the wiring command has always anchored its own, and the parent directories are created on the way.

  • logging: Behavioural change: the json record's timestamp is written to a fixed width in UTC, so the stamps sort as text into the order they were written, where RFC3339Nano trimmed trailing zeros and a record on a whole second sorted after the fractional ones of that second.

  • logging: Behavioural change: LogError anchors the record on the error the caller handed over.

  • logging: Behavioural change: LogOnRecoverAndExit and LogOnRecoverAndExitAfter refuse an exit code outside 1..255 with a panic naming the rule — the rule exception.NewExitError enforces, applied to the code the handler itself would exit with.

  • session: Behavioural change: RegenerateSession returns a storage failure met while probing the fresh id, naming the door with sessionRef and the failure as its cause, where it panicked with the raw failure; NewSession, without an error return, still panics; the malformed-id refusals of regenerate and delete carry sessionRef as the save refusal does.

  • security: the jwt validator's revocation refusal says the token was not issued after the revocation epoch and its skew, where it said "before the epoch" for a token stamped inside the skew after it, and carries issuedAt, epoch and skew; the subject refusal says the claim is absent, not a string or empty and names the claim; the access-control refusal of a repeated PUBLIC_ACCESS says "or repeated" and carries the attributes it counted; the refusal of an out-of-range matching mode carries the number beside the name String() folds to "unspecified".

  • container: MustGetByType and FromResolverByType write the requested type into a failure's context only where no nested failure named one, as their name-keyed twins write the name, where they overwrote it and a provider's failed dependency was reported as the registered service it was asked through; a value type requested where its pointer is registered is refused naming the type asked for beside the canonical one it resolved (expectedType, canonicalType, actualType), where the two read as equal; the refusal of a service that vanished after its creation keys it creatingKey as its siblings do; a scope's closed refusal at the store carries the type beside the name, and the override's closed refusals, at the entry and behind the lock, name the service.

  • event: the three subscriber-planning refusals carry subscriberType and the dispatch refusal of an empty event name carries eventType, where the four shared one keyless sentence.

  • http: the controller-signature refusals past the function check carry the signature under type, beside controllerType where they carried it so (where they carried the kind func, nothing or the expected shape alone), the argument-count refusal counts the arguments and the result-count refusal says two results and counts them; an empty single-segment wildcard is refused as a wildcard; an embedded public directory that is a file is refused apart from a missing one.

  • application: the refusal of an executable path whose symlinks cannot be followed carries the path as handed, where it carried the empty resolved one; the after-boot refusals of RegisterHttpMiddlewareFactories, RegisterService and RegisterScopedService name their own door and the service; the log-directory refusal names the directory beside the file.

  • config: a blank http address is refused as empty, where the : prefix turned it into a port refusal over an address nobody wrote; the template refusals key the reading parameter parameterName throughout (seven sites spelled parameter), the undefined-environment-key refusal included; the request body bound's refusal names the field.

  • validation: the seven tag-grammar refusals sharing "invalid validation tag syntax" carry a reason.

  • validation: Behavioural change: LessThan and GreaterThan answer a non-nil value whose kind is not numeric with the code notNumeric (ConstraintLessThanErrorNotNumeric, ConstraintGreaterThanErrorNotNumeric), where it carried the bound's own greaterThanMax or smallerThan, so a client or a translation keyed on the code could not tell a malformed value from one out of range; a numeric value past the bound, a nil pointer and a NaN keep the bound's code.

  • cache: the remember timeout refusal names the wait, not the callback, which keeps running; a counter over a payload that is not an int64, textual or past its range, is refused by Increment, Decrement and Manager.GetCounter with the payload's length, the payload kept out of the cause chain too, where strconv's refusal quoted it whole.

  • mailer: the mail and rcpt refusals carry the relay's address; the verdict at the terminating dot is named as the acknowledgment's failure, not a flush's.

  • logging, internal, storage, messagebus, translation: the correlation logger's refusals name it; a non-object JSON target is refused as such; the storage object's directory and close refusals have their own sentences; a function handler refuses a nil message instead of panicking; the translation file refusals carry the directory.

  • messagebus: the consumer's two recovery boundaries read a panic value through exception.PanicCause, as every other boundary does, so a handler panicking with a typed-nil error takes the path of a non-error panic, its dispatch error carrying the recovered value without a cause, where the typed nil travelled as the cause and its Error() would dereference a nil receiver.

  • http: a backplane swap under publish load no longer kills the process with sync: WaitGroup is reused… — the publishes holding a backplane are counted per installed generation, so a clear or a shutdown waits on the one it removed alone; a body reader whose Close panics on the gzip goroutine or the unwind watcher is contained by the quiet close, while one closed on the request goroutine, a body shorter than the compression minimum, reaches the kernel's recovery as a handler panic.

  • container: Behavioural change: GetByType refuses once the teardown finished, as Get does. Its memoized read answered the instance the teardown had already closed, where Get answers the container-is-closed error. A value a provider finishes building after Close began is still closed rather than leaked, and the failure of that close now joins the refusal, where it was dropped.

  • session: Behavioural change: Session.Clear ends the session and the ending latches. RegenerateSession clears the session it rotated away through the same latch

  • session: Behavioural change: NewManager refuses a negative ttl.

  • security: Behavioural change: two firewalls declared under one name are refused, by the builder at the second declaration and by Compile (security firewall name is already declared); the registry answered the first of them and the manager the last.

  • security: Behavioural change: a refusal on the access-control and resolution paths writes a fail-closed response instead of a nil the kernel reads as "no decision" and proceeds past.

  • lock: Behavioural change: RunExclusive and LeaderGate cancel the callback's runtime at the lease's demotion instant — half a renewal cadence, a quarter of the ttl by default, before the lease last written lapses — when no renewal has landed, where an unanswering or failing store demoted the caller only at the lapse and the next leader's OnElected ran before the first was told to stop; each renewal is bounded to that instant on its own goroutine and a failed one retried once, a callback done by then is a clean run, and a later renewal no longer saves the lease.

  • lock: Behavioural change: a LeaderGate starts renewing its lease before OnElected runs, and the hook receives a runtime whose context is cancelled when the lease is lost.

  • http/static: Behavioural change: an explicit cache max age of zero is honoured as Cache-Control: public, max-age=0 — always revalidate, with the ETag and Last-Modified machinery intact — because the configuration door validates zero as a distinct choice.

  • debug: Behavioural change: debug:router, debug:events, debug:parameters and debug:middleware apply --limit and --offset to the items they render instead of only reporting them in the payload.

  • messagebus: Behavioural change: melody:messagebus:consume separates the shutdown signal from the handler lifetime — the signal stops the pull of new deliveries, while in-flight handlers and their Ack/Nack keep a live context for the whole WithShutdownGrace window.

  • wiring: Behavioural change: a malformed exclude pattern is refused before anything is walked, and an exclude that matched no constructor is reported — and fails --strict — the way an unused bind is.

  • wiring: Behavioural change: an unknown //melody: directive is a hard error naming the file, the line and the text, instead of being silently ignored.

  • wiring: Behavioural change: a directive with a blank after its slashes, // melody:ignore, is refused naming the line and the expected spelling, where it read as prose and failed open (an ignored constructor registered, a scoped one made a singleton); an --out reaching a scanned directory through a symlink, or a directory scanned through one, is refused as inside it.

  • bag: Behavioural change: a key present with a nil value reports as unset from String, StringSlice, StringStrict and StringSliceStrict, matching what Int, Bool, Float64 and Duration already reported for the same state.

  • validation: Behavioural change: the parsed validate tag and the constraint a parameterized rule resolves to are memoized instead of being rebuilt for every value the validator reaches.

  • translation: Behavioural change: NewManager keeps every catalog of a locale, in the order given, and a lookup asks them in that order until one answers, so an application that relied on the last catalog replacing the earlier ones now gets the earlier ones answering first, where every message living only in the first answered its raw id; a catalog whose Locale() is empty is refused as the nil one is.

  • translation: Behavioural change: a .json file in the translations directory whose name does not parse as <domain>.<locale>.json is a hard error naming the file.

  • exception: Behavioural change: NewHttpException and NewHttpExceptionWithCause refuse an informational status, admitting [200, 599]: an exception raised with 103 was served as a 200 carrying the error body, and a program constructing a 1xx exception now panics at construction, a 1xx answer being written as a response; a status above 599 is refused at construction too, outside the final statuses HTTP defines; net/http's WriteHeader panics only outside [100, 999], deep in the response path.

  • application: Behavioural change: a route queued from inside a module boot hook is refused.

  • application: Behavioural change: a module registered from inside a module boot hook is refused.

  • application: Behavioural change: RegisterConfiguration refuses any name but logging.

  • application: Behavioural change: an http process whose .env artifacts contributed no keys at all refuses to boot.

  • application: Behavioural change: a middleware factory that yields nil, or a typed nil, fails the pipeline build naming the definition.

  • application: Behavioural change: a teardown failure on Run's normal return exits non-zero.

  • application: Behavioural change: a relative MELODY_LOG_PATH is anchored to the project directory, through the helper the logs and cache directories already use, and the file's parent directory is created before the open.

  • application: a static exclusion of MELODY_STATIC_EXCLUDED_PATHS that carries a percent escape other than %2F is named in the boot warning http.static.excludedPathPercentEncoded at the http boot, as the security documentation promised and as a rule path or firewall prefix already was; the file server compares the entry with the request path decoded, so such an entry excludes only a path whose name holds the percent sign literally.

  • config: Behavioural change: a ${...} reference in an .env file resolves against every file the environment loaded, and an undefined, self- or mutually-referencing one fails the boot.

  • http/cors: Breaking: cors.NewService reads a nil method or header list as the one default DefaultService grants — Authorization included — and keeps an empty list as the expressed preference, as the origins field does: an explicitly empty (non-nil) AllowOrigins, AllowMethods or AllowHeaders list denies instead of becoming the wildcard or the default.

  • http/cors: Behavioural change: a schemeless allow entry is port-significant, where extractOriginHost kept url.Hostname(), which cuts the port, so app.example.com matched https://app.example.com:9000 and any port on that host, a grant RestrictiveService couples with credentials.

  • http: a validation refusal whose errors blame the declaration — a struct tag naming a rule that does not exist — is recorded at error by the kernel as it files the handler's error, where it was filed at warning among the deliberate 4xx and marked logged before the exception listener, which carries the rule, could raise it; a refusal blaming the submitted value keeps its warning, as the http document says and as the first and second majors already did.

  • http: the warning the server-sent event hub writes when clearing its backplane stopped waiting at its timeout names how many publishes were still inside, as the http document says, where it carried the reason alone.

  • http: Behavioural change: UrlGenerator.GeneratePath treats a single-segment *name wildcard as it treats a :name parameter — a value supplied empty takes a non-empty route default, and with none left generation fails with wildcard parameter may not be empty —, where the empty value was emitted as an empty segment, a // url the kernel refuses before it routes.

  • logging: NewStandardErrorLogger treats a typed nil held in the logger interface as inert, as it treats an untyped nil, where the adapter called it and panicked on the goroutine net/http writes its ErrorLog from.

  • event: Behavioural change: a panic the dispatcher's outer recovery describes as event dispatch panicked carries an error-shaped panic value as the error's cause, through exception.PanicCause, as a listener's recovery already does, so its own context and cause chain reach the record and an *exception.HttpException panicked from an event's own method answers its own status, as one a listener or a handler panics with does, where the value sat in the context alone and the request was answered 500.

  • wiring: melody:wiring:generate refused for an output path it cannot read, relate to a scanned directory or write writes the refusal followed by its cause chain, control characters escaped, to the command's error stream, where a run whose output is redirected still shows it — the permission, the missing directory, a directory named as the file —, keeping the error's message for the journal, where the terminal printed the refusal alone.

  • documentation: eighteen further fixes — the guides and GoDoc correct statements the code had outgrown (bag.String, an empty MELODY_ENV, a malformed Accept, RunExclusive cancellation, exit budgets), state conditions they left out, document long-exported symbols and close their examples' containers.

  • cli: nineteen further fixes — a short write or print failure in the json and table printers fails the command; the run banner honours quiet, banners keep --no-color and the not-found header escapes argv text; a panicking command prints the failed banner and an exit-coded error exits through the application's shutdown and log path; the standard integer flags refuse a negative value.

  • example: thirty further fixes — the example application answers failed lookups, oversized bodies and store outages with the right status and a journaled cause; account and product writes stop losing updates, reusing deleted identifiers or leaving stale cache; migrations survive interrupted runs.

  • http: a hundred and four further fixes — the server-sent-event hub and writer close, flush and hand on their backplane safely; the kernel closes discarded responses, recovers more panics, keeps a handler's status and fails closed ahead of a required listener; error bodies follow Accept; routes, static paths and forms refuse bad input.

  • openapi: thirty-one further fixes — the generated document describes the routes the router actually serves (optional, catch-all and colliding paths, any verb); schemas mirror the validator; the registry no longer aliases callers' memory and refuses a Describe once serving; melody:openapi:generate writes clean, byte-stable output.

  • cache: fifteen further fixes — SetMultiple and Many name every refused or corrupt key; Remember refuses a value it cannot round-trip before storing it, gives each coalesced caller its own copy and reads typed-nil errors as success; the in-memory backend holds its locks for less time.

  • logging: forty-four further fixes — loggers survive self-referencing contexts, a panicking Error() or MarshalJSON and typed nils; joined errors keep every branch; control and C1 characters are escaped; the json logger stops locking around caller code; the exit handler bounds every step.

  • container: seventy-seven further fixes — teardown closes every service (CloseWithContext-only, zero-size, evicted overrides, scoped) in a sound order, without false cycle reports or races, and records who spent the deadline; Lazy handles stop racing or dying early; typed-nil errors read as success.

  • session: twenty-four further fixes — the file storage refuses unusable handles and snapshots, survives partial flushes and sweeps orphaned temp files; Set copies at the depth Get and All copy at, maps and slices; a rotation no longer logs the user out and a failed deletion no longer buries the session; InMemoryStorage refuses use after Close.

  • security: twenty-one further fixes — nil and typed-nil rules, matchers, tokens and requests no longer panic; configuration slices and secrets are copied; JWT revocation and nested impersonation are corrected; a failing may-skip listener no longer bypasses access control and a dispatcher that cannot arm the required-listener mark is named at boot; failures are filed once.

  • lock: fifteen further fixes — RunExclusive dates the lease from the issued request and the leader gate keeps its deadline in monotonic time, so a silent renewal cannot leave two leaders; a panicking OnElected ends the term with its stack, a panicking callback releases after the renewal in flight; lost terms and failed releases are logged.

  • exception: ten further fixes — an error whose Error, Unwrap, As or Context panics no longer takes down the recovery reporting it; nil receivers answer safely; LogContext walks joined errors and the top error's own chain; the mutable parts are locked; NewExitError refuses codes outside 1 to 255.

  • http/static: eight further fixes — the filesystem mode serves a public directory spelled . or / and refuses paths that fold into the mount root; the embedded mode checks its directory at construction and derives cache validators without a fake modification time; the configuration is copied at construction.

  • storage: four further fixes — LocalStorage.Put sweeps a key's abandoned temp objects safely and in batches, stores a key whose name fills a whole path component, and names a failed copy plainly.

  • debug: twenty further fixes — debug:container shows why a service failed to build, renders error contexts through their own marshalers with cycle and depth guards, and shows the teardown order and lifetimes right; debug:events counts and windows its listeners right; debug:router no longer crashes.

  • messagebus: ten further fixes — a nil or typed-nil transport is refused at registration, and one whose Close panics no longer stops the others closing; the in-memory transport refuses a negative buffer and logs dropped requeues; retry delays no longer wrap on 32-bit builds; the consume command no longer races itself.

  • wiring: fifteen further fixes — melody:wiring:generate prints only compilable source on stdout and journals its report; empty or malformed //melody:service and //melody:bind directives and colliding constructors are refused at generation, not at boot; output paths are checked strictly.

  • internal: six further fixes — the deep copy behind sessions, claims and route attributes no longer panics when a defined map or slice type and its plain form share one value; query redaction keeps the order and repeats the client sent; the atomic file writer syncs and keeps the file's mode; escaping is cheaper.

  • bag: five further fixes — String, Int, Bool and Float64 read a repeated request key as its first value instead of panicking or failing; a non-string scalar read as a string reads as absent; the zero ParameterBag accepts a write; Has no longer copies.

  • validation: twelve further fixes — shared pointers are walked once per depth instead of exponentially; diamond-embedded fields are validated again; NewRegex("") refuses rather than matching everything; float bounds are exact; typed-nil constraint errors read as success; memos are race-safe.

  • serializer: six further fixes — Accept negotiation honours the most specific range and q=0, refuses a header cut by the member cap and serves the default for an unreadable one; typed-nil targets are refused; the soft resolvers no longer panic when the logger is missing.

  • translation: two further fixes — NewManager refuses a nil catalog instead of answering raw message ids, and the empty domain resolves to the default.

  • application: thirty-five further fixes — an error handler installed after Boot is honoured; message bus transports close in every process, after their buses; shutdown drains request scopes and reports each failure once, even when hooks panic or closes race; boot refuses typed-nil modules and commands.

  • event: sixteen further fixes — typed-nil events, subscribers and listener errors are handled through the interface; an ExitError keeps its exit code through the recoveries; panic values travel as causes and are logged once; the adapter passes the original event and keeps field-less subscribers apart.

  • httpclient: thirteen further fixes — errors name the failed request while urls in them drop userinfo and query secrets; a base-url client refuses targets leaving its origin; queries reach the server as written; byte bodies are copied; credentials and timeouts behave as asked; nil options are refused.

  • config: fourteen further fixes — templated runtime parameters resolve at boot without races or half-made leftovers, while plain values holding a percent read before it; a late MarkSecret covers every derived parameter, .env expansion and kernel.* alias; the deep copy is linear.

  • http/cors: four further fixes — the preflight is answered ahead of the security chain and only when it carries Access-Control-Request-Method; Vary: Origin is sent on every response; a credentialed configuration with AllowOriginFunc no longer panics.

  • http/pipeline: one further fix — Builder.Describe lists a pipeline without running its factories and mirrors Build's nil-factory refusal, and a definition keeps its own copy of its ordering constraints.

  • mailer: three further fixes — SmtpTransport.Send refuses a typed-nil runtime cleanly, MAIL FROM and RCPT TO failures are no longer reported as rejections, and the quit warnings carry their cause.

  • clock: four further fixes — FrozenClock.Advance panics on a negative duration, a stopped frozen ticker mints no further tick, the must-resolvers refuse nil arguments by name, and the Clock and Ticker contracts are documented.

  • runtime: one further fix — runtime.New and the runtime resolvers judge a typed-nil argument through the interface and refuse it as they refuse the untyped nil.

Security

  • example: Behavioural change: the sign-in door reads an application/json body alone and answers any other with 415, before the credentials are read and without a login failure: a urlencoded form, which a page on another site can post, needed an anti-forgery token the door never had. A client that posted the form encoding sends {"username": …, "password": …}.
  • example: APP_SESSION_COOKIE_SECURE=always marks the session cookie Secure on every response, through the kernel's session cookie policy, for a proxy terminating tls outside the ranges the forwarded scheme is believed from, where the cookie was issued without Secure; empty or from-scheme keeps the scheme's answer, and any other value refuses the boot naming the key.
  • example: Behavioural change: the example's credentials are configuration, and outside development the boot refuses the values .env commits: with the compiled-in secrets of /secure and /internal, a deployment with every key changed accepted a token signed with the public constant carrying scope: {roles: ["ROLE_ADMIN", "ROLE_ALLOWED_TO_SWITCH"]} and an envelope under wms-key-1. They are read from APP_JWT_SECRET, APP_INTERNAL_AUTH_SECRET (beside APP_INTERNAL_AUTH_KEY_ID and APP_INTERNAL_AUTH_APP) and APP_ENCRYPT_KEYS, the development values committed so a checkout boots, a blank one no longer taking the public key; with MELODY_ENV other than dev a blank or committed value refuses by name. An APP_ENCRYPT_KEYS entry without its id:, or id:k,<key>, is named by position, no longer carrying the key as keyId. The example's README says what to set before production.
  • example: Behavioural change: the accounts user, editor and admin, whose passwords are their names, are seeded into an empty directory in development only; elsewhere the first account comes from the new example:user:create, which reads the password from standard input and refuses it as an argument.
  • example: Behavioural change: enrolling a second factor again replaces it only for a request presenting a current factor of the enrollment it replaces, a TOTP code on X-2FA-Code or a recovery code on X-2FA-Recovery-Code, spent as the verification door spends them; on a session alone it answers 401 with the challenge, where a stolen session became a durable second factor of the thief's. An account that lost both is reset with the new example:twofactor:reset.
  • example: a deleted account no longer keeps its tokens when another listener of the deletion fails: an unreachable two-factor store ended the dispatch first, so its tokens kept authenticating on /device/*. The release outranks every listener and revokes the account's tokens at the deletion's instant; a device token issued during the deletion is taken back; the device firewall honours a token only while its account exists, with its current roles; a failed two-factor release is journaled without failing the deletion; example:db:reset empties the token namespace on Redis.
  • example: Behavioural change: an account holds at most five live sessions, where unbounded sign-ins grew every session save, the sign-in past the cap ending the oldest at the sign-in door and the firewall's login handler, through the session index the schema migration holds; a sign-out frees its place, and the cap counts live sessions only: the row of a session that ended elsewhere is dropped by the next sign-in before any live one is ended. A sign-in whose index or liveness read fails stores nothing; a row admitted in the last minute counts as live, so concurrent sign-ins cannot pass the cap, a failed save holding its place for that minute alone; without a database the index restarts from zero.
  • example: Behavioural change: an account holds at most four event streams open at once, and the process 256, counted across /events/stream/ and the /ws websocket route; a stream past either cap is refused with 429 before anything is committed and a closed stream frees its place, where one client could hold every descriptor of the process.
  • http, security: the router and every reader of the routed path — the canonical guard, the access-control listener, the firewall path matcher, the internal-auth binding, the compression exclusion and the static file server — read the path as the client sent it, while the raw path still unescapes to the decoded one: URL.EscapedPath re-spelled a raw path carrying a byte it does not keep — /admin%2Fusers{, or an encoded separator beside a raw UTF-8 byte — with a separator in place of %2F, reaching the /admin/*rest handler, where it now routes as /admin%2Fusers does, the one segment admin/users.
  • http, security: Behavioural change: the kernel refuses with a 400, after matching and before authorization, a path that leading or trailing whitespace would be trimmed from — /public%20, /public%09, /public%C2%A0, or /public from a front handler such as StripPrefix on /api%20/public — where the router served the protected catch-all under /public while the matcher trimmed it onto an exact PUBLIC_ACCESS rule of /public, an anonymous grant. Whitespace inside a path is not refused.
  • http, security: Behavioural change: the access-control matcher, the firewall path matcher and the canonical-path guard read the spelling the router matched, RequestPathAsRouted, an encoded separator kept in its segment, not the decoded URL.Path: /public%2F and /public%2F%2F, the resource public/ behind a protected catch-all, read as /public/ and were served anonymously under the PUBLIC_ACCESS rule of /public. /admin%2Fusers, never routed under /admin, is no longer claimed by a rule for /admin/; a rule names such a resource with %2F. The static file server, its exclusion list, the compression exclusion list and the internal-auth binding read it too: /static/private%2Fsecret.txt served private/secret.txt under a protected prefix judged as /static, and an envelope for /files/a/b authenticated /files/a%2Fb, which a caller now signs as it sends it. The released v3.13.0 routed on the decoded path the matcher read, so the disagreement was the branch's
    alone.
  • config: a configuration failure no longer copies a value into the boot record: a godotenv parse failure is reported by shape, not by quoting the line of the .env it failed on, and its sanitizer reads the "unterminated quoted value" shape by its prefix before cutting at " near ", where PASSWORD="hunter2 near the door with its closing quote forgotten was copied up to those words, the malformed-name failure keeping its cut; a secret parameter withholds the value-quoting strconv or parse error beneath its conversion error; a reference to a non-string parameter reports the type it found instead of the value, which went into the error context whole, and a template commonly references the parameter holding a dsn with its password.
  • config: the refusal of an unclosed %name reference or an unterminated %env( carries the parameter and the percent's byte offset and none of the value, where DB_PASSWORD=Pa%SSword1 put %SSword1 in the boot record under reference; inside a value read through %env(KEY)% it also names the key once, by the innermost read, a %parameter% there being scanned under its own name. A bounded %env(...)% candidate closing with )% that is no placeholder keeps its echo.
  • config: MarkSecret no longer leaves unredacted a value assembled through a parameter's kernel.* alias: a dsn built from a secret read through the other spelling printed in full beside the redacted key; the propagation follows every alias the parameter answers to.
  • validation: Behavioural change: the validator bounds what one Validate call costs and answers, where a 1 MiB body bought a 400 above a gigabyte: a map key past 128 bytes is spelled as its first 128 bytes, cut on a rune, plus ...(truncated); the depth cut is one entry naming the truncated list or map, no longer each element; at most 1,000 errors, then one entry under the root field "" with code errorLimitExceeded and context maxErrors, the 1,001 still refusing the value; a path is cut at its head past 256 bytes behind ...(truncated), an answer at 512 KiB with that entry carrying maxAnswerBytes. NewValidatorWithLimits configures them (see Added).
  • session: Behavioural change: RegenerateSession refuses an id a logout or an earlier rotation buried, with ErrSessionDeleted and, for a rotated-away id, ErrSessionRotated, before anything is stored: a session loaded before a logout was rotated into a fresh id carrying the old identity, and two rotations forked one session. The tombstone holds every id a delete leaves absent, a lapsed or evicted entry included; the response path deletes only the session the cookie named; a delete reported failed is remembered once the entry is gone.
  • httpclient: the diagnostics of a url net/url refused or mis-read no longer carry its password: an "@" past the first "/", "?" or "#" of a refused url's authority keeps only what precedes it (http://xxxxx); every quoted span of the parse error is redacted, which quoted a password head read as a port (invalid port ":pa" after host), an "@" written %40 included; a parsed url with a userinfo or a port-read password and an "@" in its path, query name or fragment keeps only its scheme; an "@" in a query value stays redacted.
  • storage: Behavioural change: LocalStorage refuses, at every door (Put, Get, Delete, Exists), a key that carries a .. segment, the backslash read as a separator, and names the segment in the error, where tenant-b/../tenant-a/secret.txt read, overwrote and deleted tenant-a/secret.txt although STORAGE.md promised such a key was rejected. The base directory was never left; a key relying on the fold to address a/../b as b is now refused.
  • http: the kernel names a session cookie policy of SameSite=None with SessionCookieSecureNever once, at WARNING under bootWarning=http.sessionCookie.sameSiteNoneNeverSecure, at the first request it serves: every current browser drops such a cookie, so no session sticks. The policy is honoured as written.
  • http: Behavioural change: a request path that folds to a different spelling — //admin/panel, /open/../admin/panel — is refused with a 400 before it is authorized, handled or served by any route.
  • http: a path that does not begin with "/" is judged with the "/" prepended before the folding guard, the spelling both security matchers read: GET /apiadmin/../public, handed on as admin/../public by a prefix-stripping front handler, selected the firewall of "/admin" under the rule of "/public" and ran the not-found handler under the public rule; it is now refused with 400, as /admin/../public already was.
  • http: Behavioural change: the rate limiters key an IPv6 client on its /64 instead of its /128, on RateLimitMiddleware and RegisterRateLimitRequestListener, whether the address is the direct peer or comes from a ClientIpResolver; an IPv4 address, bare or mapped into IPv6, keys on itself. One host routinely holds a /64 and rotated its source address for a fresh budget per request, and with a key ceiling set filled the key table so every new client was refused. DefaultClientIp still answers the address itself.
  • http: Behavioural change: the access log and the kernel's 405 and no-route records keep the query parameter NAMES and redact every value, through the one internal.RedactQueryValuesForDiagnostics the security package's sanitizer shares.
  • http: Behavioural change: the access log redacts the Referer as it redacts the request's own query, dropping its fragment and user information, and cuts one that does not parse at its first ? or #: a credential in the address of the previous page, such as a reset or signed link's token, was journaled on every same-origin request that followed.
  • http: Behavioural change: the kernel refuses with a 400, before authorization, a request whose URL.RawPath no longer unescapes to URL.Path, as a front handler rewriting URL.Path alone leaves it: /api/admin%2Fusers stripped of /api on Path only reached the /admin/users handler. A front handler rewrites both fields, as nethttp.StripPrefix does, and never clears RawPath. The refusal carries no route to the kernel.response and kernel.terminate listeners, leaves one warning and no no route matched record, and carries rawPath beside path.
  • http: Behavioural change: RedirectResponse — and RedirectFound/RedirectMovedPermanently through it — refuses a location that leaves the application: a scheme (https:, mailto:, javascript:), a scheme-relative //host, or a backslash, which several browsers fold to a slash while net/url does not. RedirectExternalResponse is the new unguarded form, whose name is the caller's assertion that the target is trusted.
  • http: RedirectResponse classifies the location the header writer will emit, not the one the caller passed: net/textproto folds away leading spaces and tabs, so //evil.example.com carried no prefix for the guard to find and reached the browser as //evil.example.com, a location built from client input leaving the origin.
  • http: the rate-limit request listener no longer serves a refused request unmetered: a typed-nil response from the application's OnLimitExceeded skipped the 429 fallback and the over-budget request reached the handler; it is read through IsNilInterface, as the middleware twin does.
  • http: ConfinedFileResponse and ConfinedAttachmentResponse serve a file named by a client-steerable name confined to a root: an absolute or climbing name is refused, not folded, the joined path resolved through every symlink must stay under the resolved root (. or / as root works, as does an absolute symlink inside a relative root), and only a regular file is answered. Each refusal is one 404 HttpException with no server path in the body, the cause kept for debug mode; a blank or unresolvable root answers 500.
  • http: the static file server no longer discloses or serves more than the public files: its entity tag is a truncated sha256 digest of its derivation, where it told every anonymous client the file's modification instant and, for embedded assets, the build version string; it serves only regular files and asks before os.Open, so a FIFO in the public directory no longer parks a request goroutine forever and a device node or socket is not answered; a strip prefix together with an embedded public directory can no longer serve a file outside that directory.
  • http: Behavioural change: the static file server refuses a path element beginning with a dot, so a .env or .git/config left in the public directory is no longer served — and, with the shipped cache defaults, no longer stored by a shared cache.
  • http: DeleteCookie honours the browser's prefix contracts, so the deletion of a __Host- or __Secure- cookie actually lands.
  • security: five likely mistakes are journaled once at WARNING under bootWarning, never with a secret: at the http boot a firewall left with the empty access control (security.firewallAccessControlEmpty; an explicit WithAccessControl(security.NewAccessControl()) stays silent), a prefix firewall an earlier one claims whole, /api-admin after /api (security.firewallShadowed), a rule path or prefix with a percent escape other than %2F (security.pathPercentEncoded); at first use an HS256 JwtConfig.Secret or internal-auth HmacKey.Secret under 32 bytes (security.jwtSecretShort, security.hmacSecretShort, by key id) and a credentialed cors origin without a scheme, admitting the host over http (cors.credentialedOriginWithoutScheme). None refuses; SECURITY.md lists them.
  • security: the firewall path matcher reads a path that does not begin with "/" with the "/" prepended, as the access-control matcher does — the empty path of an absolute-form (GET http://host HTTP/1.1) or authority-form (CONNECT host:port) target, the asterisk-form GET * HTTP/1.1, a path a front handler hands without its slash — where no firewall claimed them: the empty path reached the route at "/" or a catch-all unauthenticated, and the others, which the router does not route, ran the not-found handler without a security context; under a global rule on "/" a signed-in user was refused instead, the firewall's token source never having run. The firewall that owns "/" now claims them and applies its access control.
  • security: Behavioural change: the internal-auth endpoint binding refuses a request whose path carries a segment that decodes to a literal %2F (sent as %252F): an envelope signed for /files/a%2Fb authenticated /files/a%252Fb, which the router binds to another parameter value. The signed spelling is unchanged.
  • security: Behavioural change: SecurityContext.IsGranted refuses a token that reports roles while answering IsAuthenticated false, as the role voters do; a handler branching on IsGranted rendered privileged content behind a route the firewall denied.
  • security: a typed-nil value the application hands in is read as the absence it means: a firewall whose matcher is a typed nil is skipped instead of crashing the claim walk on every request in its Matches call (NewCompiledFirewall validates nothing), and the login and logout handlers, the entry point and the access denied handler are read the same way; ResolverTokenSource.Resolve answers the anonymous token for a typed-nil token from the application's resolver, where it handed it back as a live token; the access-control listener no longer serves a request an entry point or access-denied handler refused with a typed-nil response.
  • security: Behavioural change: a regex rule carrying PUBLIC_ACCESS is refused unless every branch of its pattern is anchored to the path start, asked of the parsed expression through regexp/syntax: the textual test for a leading ^ passed ^/public|/status, read as (^/public)|(/status), whose second branch floats and granted /admin/status-board. (?i)^/public(/|$) and \A/public, which panicked at boot, are accepted now; (?m)^/public, which matches after a newline too, is refused.
  • security: Behavioural change: security.NewAccessControlRule refuses PUBLIC_ACCESS with a panic at construction, as accesscontrol.NewRawPrefixRule does: a raw public prefix opens every path that merely starts with it — /public opens /publicadmin — and shadows a bounded denial beneath it. A public area is declared with NewAccessControlRuleWithSegmentPrefix, accesscontrol.NewSegmentPrefixRule or AccessControlBuilder.AllowAnonymous; the rule without PUBLIC_ACCESS keeps its raw-prefix reach.
  • security: Behavioural change: NewHmacTokenSource refuses a negative MaxFutureExpiry, which behaved as the zero "unbounded" case and reopened the memory-pinning window the cap closes, the way JwtConfig refuses a negative revocation skew.
  • security: Behavioural change: totp.Config.Period is clamped: a period at or above 1<<63 froze the counter at 0 and verified one secret-derived code at every instant forever; a period past maxPeriod falls back to the default, as the skew does.
  • security: the api-key comparison of ApiKeyHeaderAuthenticator and ApiKeyHeaderRule runs over fixed-width digests, so its duration no longer tells a caller when a guess has the key's length (subtle.ConstantTimeCompare answers at once on unequal lengths).
  • security: Behavioural change: a firewall prefix written with a trailing slash also claims the bare spelling of its route: the router reads /admin/ and /admin as one route, but PathPrefixMatcher compared the raw path, so a firewall on /admin/ never matched /admin and the route was reachable unguarded one spelling apart.
  • security: the access control matcher folds the spellings that reach one resource into the one the rules are written in before it matches: //admin/panel and /open/../admin/panel matched no rule naming /admin, and a path no rule matches is granted without consulting the token, so a route answering those spellings (a catch-all single-page mount, a proxy, a documentation tree) served them to anyone.
  • security: PurgeExpired on the in-memory token store no longer drops the revocation boundaries of a user who holds no stored tokens: stateless JSON web tokens validated by NewJwtTokenValidatorWithRevocationEpoch are never stored, so on a JWT-only deployment the first purge after a RevokeBefore un-revoked every outstanding token of that user, and the same happened once a user's last opaque token expired.
  • security: Behavioural change: the internal-auth envelope and the JSON web token are separated by TYPE, not only by their secrets happening to differ.
  • security: a query-string mismatch on the internal-auth endpoint check journals the parameter NAMES and redacts every value, on both sides; a failed dispatch of the login-failure event keeps the authentication error and the dispatch error joined, the authentication error first, so its status is still the one served and errors.Is reaches either (for rejected credentials the dispatch error was returned alone), and a failing access-denied handler keeps its own error beside the authorization decision.