revali_router 5.1.0
revali_router: ^5.1.0 copied to clipboard
Feature rich http router for Revali
CHANGELOG #
5.1.0 | 08.15.26 #
Features #
- Add
healthRoutes, which builds the liveness and readiness routes described by aHealthSettings. Readiness takes anisDrainingcallback, read per request rather than captured, so the probe reflects the current shutdown state instead of the state at startup. Checks run concurrently, so the probe costs the slowest check rather than the sum of them, and a check that fails, throws, or outrunscheckTimeoutis reported as unhealthy with its name — a probe that 500s tells an orchestrator strictly less than one that names the dependency that is down. shutdownServertakes adrainDelay, applied after the drain is flagged but before the listening socket closes. Requests arriving in that window are served and tracked normally, since the accept loop does not refuse while draining. Defaults toDuration.zero, which is exactly the previous behaviour.- Add
WorkerFleetandlistenForDrainCommands, the two halves of a shutdown that reaches worker isolates. WithAppConfig.workers > 1every isolate binds the same port withshared: trueand keeps its own in-flight set, while only the parent watches signals — so aSIGTERMdrained one isolate and the parent'sexit(0)truncated the rest, and a readiness probe balanced onto a worker reported ready while the parent was already draining. The parent now tells each worker to drain and waits for them to report back before exiting. A worker that dies, never registers, or hangs is bounded by a timeout rather than holding the process open. - Install a
TraceContextfor every request, seeded from the caller'sX-Request-Id,traceparent,tracestateandbaggageheaders and generating a request id when none was sent. It is installed on thehandleRequestserve path — the onehandleRouterRequestsand the generated server actually use — as well as the olderhandlesplit, and for every request whether or notdiis configured: an id that only exists when dependency injection happens to be set up is one a logger cannot rely on. @RequestIdnow stamps the id the ambientTraceContextcarries rather than generating a second one, so the header and the context always name the same request. It still works outside a request, where there is no context.- Forward
AppConfig.fromEnvon this package'sAppConfig.revali_routerre-exportsrevali_corewithAppConfighidden and defines its own subclass, which is the one apps actually extend — a constructor that exists only upstream is unreachable from an app, and one that was only added there compiled in arevali_coreunit test while every real app failed with "Superclass has no constructor namedAppConfig.fromEnv". - Respond to an uncaught
HttpErrorwith its own status and error envelope. Handled after the exception catchers, so an app that registered a catcher for its ownHttpErrorsubtype still wins — the envelope is a fallback for an unclaimed error, not an override.
Fixes #
- Log the exception and stack trace behind every
5xx. An error that noExceptionCatcherclaimed was passed into the router's debug handling and then dropped: on the defaultdebug: falsepath it returned a bareInternal Server Errorand let both arguments go out of scope, so a compiled server answered500and wrote nothing anywhere. The access log records status and timing by design, and the root catch inhandleRouterRequestsonly sees errors that escape the pipeline entirely — an error the catcher chain handled never reaches it — so there was no path on which the type, the message or a single frame survived. Registering catchers for an app's own typed exceptions and nothing else is the obvious way to write them, which leaves everydart:ioexception and everyStateErrorfrom a dependency in this gap; the failure that surfaced it was aFileSystemExceptionon a Windows CI runner, diagnosable only by elimination from an adjacent request that happened to succeed.debugnow decides what the caller is shown, not whether the operator is told at all: it was equally unlogged withdebug: true, which merely put the trace in the response body, so the one configuration that made the error visible did it by disclosing internals to whoever triggered it. Gated on the status the pipeline actually produced, so the routine4xx— a404, a blocked CORS origin, a guard refusing a caller, a catcher deliberately mapping its own exception to a401— stay as quiet as they were.
5.0.0 | 08.13.26 #
Breaking Changes #
Observer.seetakes oneObservedRequestinstead of(Request, Future<Response>). This lands here as well as inrevali_core:revali_router.dartre-exportspackage:revali_core/revali_core.darthiding onlyAppConfig,BodyandLifecycleComponents, soObserveris part of this package's public API and an observer that imports it frompackage:revali_router/revali_router.dartmust migrate. The migration is mechanical — seerevali_core3.0.0.- Depend on
revali_core: ^3.0.0, which also removes the deprecatedDI.registerInstance<T>/DI.register<T>methods.
Features #
- Resolve each request's
ObservedRequest.summaryonce it completes, in every mode. The existingRequestTracering buffer and inspect log stay gated ondebug/inspect, but telemetry is not debug tooling — gating it there would leave production with none. Observers are not awaited, and a throwing one is logged rather than allowed to affect the response or the other observers. - Add the
@Throttlekit: rejects a caller exceedingmaxrequests perwindowwith429, carryingRetry-After,X-RateLimit-LimitandX-RateLimit-Remaining. Callers are identified by client IP (resolved throughtrustedProxy), and allowances are bucketed by the matched route's registered path —/api/users/:id, not/api/users/42— with an optionalbucketto pool several endpoints. It is a fixed window held in memory, so state is per process; documented as such rather than implied to be cluster-wide. NamedThrottlerather thanRateLimitdeliberately: the barrel is imported wholesale, andRateLimitis a name apps already use for their own components. - Gzip responses through the default response handler, negotiated via
Accept-Encodingand configured withRouter.compression. Deliberately conservative: only bodies of a known length are compressed, which leaves streaming and SSE responses untouched — gzip buffers, so compressing a stream would hold back chunks the handler meant to flush. Partial content (206) and already-encoded responses are skipped, and compressed responses carryVary: Accept-Encoding. Routertakes an optionaldi. When set, every request runs with its ownRequestScopedDIinstalled for the whole pipeline — middleware, guards, interceptors, the handler and exception catchers all resolve against the same scope. Disposal waits until the response has been fully written, so streaming and SSE handlers keep their request-scoped resources for as long as they are sending. Omittingdileaves requests unscoped, which is how aRouterbuilt directly in a test behaves.- Add graceful shutdown.
InFlightRequeststracks requests the accept loop detached,shutdownServerstops the listener and waits for them within a timeout before forcing the socket closed, andlistenForShutdownruns a callback on the firstSIGTERM/SIGINT(ignoring later ones while a shutdown is already running, and skippingSIGTERMon Windows, which has no such signal).handleRequestsandhandleRouterRequeststake an optionalinFlightand behave exactly as before without it.
Fixes #
- Stop
Router.close()throwingConcurrent modification during iteration. Each registered cleanup removes itself from the list as it runs, so walking the live list was unsafe wheneverclose()happened with requests still registered. Previously unreachable, becauseclose()only ever ran once everything had already drained. - Stop
BodyImpl.read()from leaking the response body's source stream subscription when its listener cancels early.asBroadcastStream()defaults to pausing (not canceling) the source when the last listener drops, in case a future listener resumes it later -- but a response body is only ever read once, so the paused subscription, and whatever it held open, never got released.
4.0.2 | 08.06.26 #
Fixes #
- Stop
BodyImpl.read()from leaking the response body's source stream subscription when its listener cancels early.asBroadcastStream()defaults to pausing (not canceling) the source when the last listener drops, in case a future listener resumes it later -- but a response body is only ever read once, so the paused subscription, and whatever it held open, never got released.
4.0.1 | 08.06.26 #
Fixes #
- Stop
Routerfrom retaining a cleanup closure per request for the life of the process. Under sustained load this was an unbounded memory leak that never released until the server restarted, even on requests with nothing to clean up.
4.0.0 | 08.04.26 #
Breaking Changes #
revali_router_coreandrevali_router_annotationsno longer exist as separate packages (both deprecated) — depend onrevali_core: ^2.0.0andrevali_annotations: ^3.0.0directly.revali_router's own public API is unchanged; only the import source of the re-exported types moved.
Features #
- Add
@RequestId()lifecycle kit to ensure every request has an ID header (defaultX-Request-Id). - Add request inspect / timing traces for
dev --inspect. - Plumb
AppConfig.workersandAppConfig.backlogthrough the routerAppConfig.
Fixes #
- Map
MissingArgumentExceptionto HTTP 400 (with richer expected/actual type detail). - Include empty-path child routes in OPTIONS
Allowheaders. - Bind
Setand coerced query parameters correctly. - Harden the request accept loop against handler failures.
Enhancements #
- O(1) static route lookup; single
Findper request. - Cache UTF-8 bytes for JSON/string response bodies.
- Cache HTTP
Date(~1s) and skip empty CORS / middleware / guard / interceptor stages. - Add configurable
DefaultResponses.badRequest.
3.4.0 | 06.17.26 #
Features #
- Add
RequestWrapperlifecycle component that wraps the entire request pipeline in setup and teardown logic. - Configure
trustedProxyon the app to resolve client IP from reverse-proxy headers (e.g.X-Forwarded-For). - Support wildcard path parameters (
*restand bare*). - Allow underscores in route path segment names.
- Support
Stream<List<int>>byte-stream request bodies.
Fixes #
- Fix coercing nested maps and lists.
- Fix route matching when path segments contain apostrophes.
Enhancements #
- Add stack traces and request context to exception handling.
- Handle uncaught errors in the request pipeline.
3.3.0 | 05.21.26 #
Features #
- Expose client IP address via
request.ip, derived from the connection's remote address.
3.2.1 | 05.18.26 #
Fixes #
- Issue where
coercewould incorrectly coerce JSONnullvalues tonullin maps.
3.1.0 | 04.28.26 #
Features #
- Cover
AppConfig.runStartupwith a default implementation that forwards to the provided start callback.
3.0.7 | 03.05.26 #
Fixes #
- Fix dynamic routes like
/:paramincorrectly matching a static sibling route when extra path segments exist: only return parent when remaining path segments are empty so the correct dynamic route is matched
3.0.6 | 03.03.26 #
Fixes #
- Fix OPTIONS returning 404 for prefix routes (e.g.
/api) by returning the prefix route when path matches exactly and method is OPTIONS - Fix OPTIONS returning 404 for paths like
/api/forums/member/:idwhen a static sibling route (e.g.member) partially matches: continue trying other routes instead of returning when recursion yields no match
Enhancements #
- Aggregate allowed methods from descendant routes for prefix routes (no handler) so OPTIONS responses include correct
AllowandAccess-Control-Allow-Methodsheaders
3.0.5 | 02.18.26 #
Fixes #
- Fix header getter pattern matching for multi-value headers
- Skip empty header values in
forEachcallback - Fix
CookiesImpl.headerValue()to useentriesfor proper inheritance
Enhancements #
- Add default values for SetCookie attributes (httpOnly, secure, sameSite, path)
- Separate cookie values from SetCookie attributes in
SetCookiesImpl - Change
SetCookiesImpl.securefrom nullable to non-nullablebool
3.0.4 | 02.11.26 #
Fixes #
- Fix route matching for
OPTIONSrequests on dynamic endpoint paths (e.g.:id)
3.0.3+1 | 01.31.26 #
Enhancements #
- Add optional param to
headers.set(expose: true)to expose the header to the client
3.0.3 | 01.31.26 #
Enhancements #
- Add optional param to
headers.set(expose: true)to expose the header to the client
3.0.2 | 11.22.25 #
3.0.0-dev | 09.19.25 #
Breaking Changes #
- Drop all custom contexts based on lifecycle component
- Create a generic
Contextinterface to replace all custom contexts - Use new types from
revali_router_core
2.4.1 | 08.26.25 #
Fixes #
- Issue where allowed headers were not inherited properly
- Issue where allowed headers could block requests with unknown headers
2.4.0 | 08.16.25 #
Features #
- Create new
addmethod toMutableCookies - Add clean up to router close method to prevent memory leaks
2.1.0 | 04.07.25 #
Features #
- Explicitly check for binary types when resolving body
- Clean up resources after response has been handled
- Support sending data asynchronously
- As opposed to only on an event received
Enhancements #
- Check for
nullvalues in addition toNullBodybody data types - Handle exceptions when resolving body
- Coerce body types when no mime type is provided
- Improve path parameter extraction
- Force sequential execution of sent
WebSocketmessages
Fixes #
- Issue where crash would occur during SSE when connection was closed by client unexpectedly
- Issue where endpoint path would result in 404 when parent controller's path was empty
2.0.1 | 03.26.25 #
2.0.0 | 03.26.25 #
Breaking Changes #
- Remove
UnknownBodyData, will default to aByteStreamBodyDatainsteadUnknownBodyDatahad the potential to hang if the body was a open stream
Features #
- Create
WebSocketContextclass for context management ofWebSocketconnections- Specifically
closeing the connection
- Specifically
- Allow empty paths for parent routes when their handler has not been set
1.7.0 | 03.24.25 #
Features #
- Add support for primitive body types
int,double,bool
Enhancements #
- Clean up resources after request is complete
Fixes #
- Issue where streamed responses were not encoded correctly
- Issue where body could throw exception during
setting- Now catches and sets status code to 500
- Issue where on connect was not being called for
WebSocket
1.6.0 | 02.07.25 #
Features #
- Use
.thensyntax instead of await to handle request operation- This allows for faster request handling
1.5.0 | 01.27.25 #
1.4.1 | 01.27.25 #
1.4.0 | 01.20.25 #
1.3.0 | 12.11.24 #
1.2.0 | 11.21.24 #
Features #
- Create
ExpectedHeadersas non-optional headers to be passed into the request - Add
ExpectedHeadersto access control headers
Enhancements #
- Re-order the pre-request checks to
- CORs Origins Validation
- CORs Headers Validation
- (CORs) Expected Headers Validation
- Options Request Handling
- Redirect Handling
- Return actual response in the
OPTIONSrequest instead of a canned response - Handle internal root errors with the response handler instead of deprecated
sendmethod
Fix #
- Add
routesparam toSseRouteconstructor
1.1.0 | 11.18.24 #
Features #
- Support advanced
ResponseHandlerper route- If a response needs to be handled differently for a specific route, a
ResponseHandlercan be provided to the route to send the response to the client
- If a response needs to be handled differently for a specific route, a
- Create default response handler for
Router - Create
SseRoutefor Server-Sent Events - Create
SseResponseHandlerfor Server-Sent Events
Enhancements #
- Improve how streams are prepared for sending to the client
Chores #
- Upgrade dependencies
1.0.0 | 11.14.24 #
- Initial Release