quadrant_server 2.5.2
quadrant_server: ^2.5.2 copied to clipboard
A Dart-first, batteries-included HTTP server framework built on dart:io with zero external dependencies.
2.5.2 #
Fixed #
- Memory spikes on request bodies —
bodyParser(),formParser()andmultipartParser()collected the body in aList<int>, which takes 8 bytes per byte plus room to grow. They now useBytesBuilder, andmultipartParser()no longer copies each file twice. A 45 MB multipart upload (rejected with413by the default 10 MB file limit) used to raise process memory to about 640 MB; it now stays around 140 MB. staticFiles()loaded whole files into memory — Files are now streamed from disk. Serving a 200 MB file used to take the process to 256 MB of memory; it now stays at 12 MB.WebSocketGroupkept closed connections — A closed connection stayed in its group until the nextbroadcast(), unlessonClosecalledremove(). Members now leave the group by themselves when the connection closes. To make this work, the server now closes its side of a WebSocket when the client leaves, sosocket.donecompletes. Callingremove()still works.- Errors in user callbacks crashed the server — An exception thrown in a WebSocket
onMessageoronClosecallback, or in the server's ownonError, ended the whole process. WebSocket callback errors now go to the route'sonError(or are printed). A throwingonErrornow produces a plain500. A throwingonCloseno longer makesclose()hang. IfonStartthrows, the socket is now closed with code1011; before, it stayed open with nobody listening. - Malformed query strings crashed the server — A request such as
/?a=%FF(invalid UTF-8) threw before any handler ran, so any client could stop the process. Such bytes are now decoded asU+FFFD.
Documentation #
SseContext.from(keepAliveInterval:)— Disabling the keep-alive pings means a client that went away is only noticed on the nextsend(), so the handler stays in memory until then. This is now documented.
2.5.1 #
Security #
- Swagger UI SRI — The
integrityvalues shipped in 2.5.0 for/quadrant_docswere malformed and the CDN URL used a floating@5tag. The page now pinsswagger-ui-dist@5.33.1with valid SHA-384 hashes. staticFiles()— The root-boundary check now requires a path separator, so a sibling directory whose name starts with the root's name (for example/srv/public-privatefor root/srv/public) is no longer reachable.multipartParser()— NewmaxTotalBytesoption (default 50 MB) caps the whole request body, checked againstContent-Lengthand again while streaming. Previously only per-file size was checked, after the full body had been buffered.
Fixed #
- Dropped cookies —
cors()andrateLimit()no longer discardSet-Cookievalues set withResponse.withCookie(). StreamResponsethrough middleware —helmet(),cors(),requestId()andrateLimit()no longer replace aStreamResponsewith an empty body.compress()middleware order — Compressed bodies now survive outer middlewares. Previouslycompress()only worked when it was the first middleware; otherwise clients received a gzip header with an empty body.bodyParser()/formParser()413 path — Removedreq.raw.drain()calls that ran after the request stream was already cancelled.rateLimit()— Idle clients are now evicted from memory. The docs now call it a sliding-window limiter (it never used a token bucket).
Added #
Response.copyWith(), overridden byStreamResponse;StreamResponsenow acceptscookies.multipartParser(maxTotalBytes: ...).
Changed #
compress()now returns aStreamResponse. The internalconsumeCompressedBodyhelper was removed.
2.5.0 #
Security & Hardening #
Response.redirect()validation — Replacedassertwith runtimeArgumentErrorvalidation so invalid status codes throw in release mode.bodyParser()error propagation — Narrowed body parser try-catch toon FormatExceptionso low-level socket and connection errors are not swallowed. AddedmaxBodyBytesguard to defend against body-size DoS (returns413 Payload Too Large).cors()preflight caching — AddedmaxAgeparameter (defaults to 86,400s) emittingAccess-Control-Max-Ageheader on OPTIONS preflights to prevent repeated OPTIONS round-trips.docs.dartCDN asset integrity — Added Subresource Integrity (SRI) hashes (integrity&crossorigin) to Swagger UI CDN scripts and stylesheets.- Static files path traversal security — Path resolution in
staticFiles()now normalizes paths with URI semantics to prevent directory traversal escapes (..). docsLocalOnlyoption wiring — Fixed constructor parameter passthrough inQuadrantServersodocsLocalOnly: falseis honored when intentionally deployed behind a proxy.
Added #
helmet()middleware — Injects 8 essential HTTP security headers (X-Content-Type-Options,X-Frame-Options,Referrer-Policy, etc.).requestId()middleware — Generates and forwardsX-Request-ID(UUID v4) for distributed request tracing, exposed inreq.context['requestId'].rateLimit()middleware — Sliding-window rate limiting per client IP or custom key withX-RateLimit-*andRetry-Afterheaders.bearerAuth()middleware — Extracts bearer token, validates against async callback, and injects principal intoreq.context['user'].ipFilter()middleware — Allow-list or deny-list client IP addresses supporting exact match and IPv4/IPv6 CIDR ranges.cookieParser()middleware & Response cookie helpers — ParsesCookieheader intoreq.context['cookies']; addedResponse.withCookie()andResponse.clearCookie()withSet-Cookiegeneration.formParser()middleware — Decodesapplication/x-www-form-urlencodedpayloads intoreq.bodywith configurable byte limits.multipartParser()middleware — Handles multipart file uploads and form fields, exposing typedUploadedFileinstances inreq.context['files'].compress()middleware — Response gzip compression usingdart:ioGZipCodecwithAccept-Encodingnegotiation.etag()middleware — Generates weak ETags and handlesIf-None-Matchconditional requests returning304 Not Modified.SseContext— Push-based Server-Sent Events (SSE) compatible with browserEventSourceand automatic keep-alive comment pings.StreamResponse— Response subclass for streaming raw bytes directly to client sockets without memory buffering.- Request validation DSL —
BodySchemaandField(string,int_,double_,bool_,list) withvalidate()middleware returning422 Unprocessable Entity. listenSecure()— First-class HTTPS/TLS binding withcertificateChainandprivateKeyfiles.- Health check endpoint — Optional built-in
/healthreporting uptime, request counts, and RSS memory usage. - WebSocket heartbeats — Configurable
heartbeatduration onWebSocketRoutefor automatic ping/pong keepalive. - Graceful in-flight request draining —
app.close()drains active requests before terminating. timeout(Duration, {onTimeout})middleware — Races the handler chain against a deadline; returns408with{"error":"Request Timeout","timeoutMs":…}or youronTimeoutresponse.staticFiles(rootDir, {indexFallback})route handler — MIME detection,ETag/Last-Modifiedwith304forIf-None-Match/If-Modified-Since,Cache-Control: public, max-age=3600,index.htmlfallback.healthCheckPathoption — Default/health. The endpoint is answered before middleware runs, so it skips auth, rate limiting and logging.app.close({force})—force: trueskips draining in-flight requests.- TLS/binding options —
listenSecure(password:)for encrypted keys;listen/listenSecureacceptaddress. - Validation rules —
required,minLength,maxLength,matches(RegExp),email,oneOf,min,max,minItems,maxItems. String inputs are coerced for int/double/bool fields. Failures return422 {"errors": {field: message}}. SseContextAPI —send(id:, retry:),sendComment(),close(),isOpen,keepAliveInterval(default 15 s,Duration.zerodisables),X-Accel-Buffering: no.WebSocketGroup—exclude:onbroadcast/broadcastJson, plusisEmpty,isNotEmpty,length,members.- Middleware options —
bearerAuth(contextKey, realm)withWWW-Authenticateon 401;ipFilter(onDeny);multipartParser(maxFileSize, filesContextKey, fieldsContextKey)returning413for oversized files;bodyParser/formParseracceptmaxBodyBytes: 0to disable the limit; all 8helmet()headers are overridable andnullremoves one.
Fixed #
- WebSocket upgrade error handling — Upgrades failing in
onErrornow return an HTTP error response to the client rather than leaving the connection hanging. - WebSocket middleware chain sentinel — Replaced dummy 200 response with
Response(204)sentinel. - Connection close resilience — Guarded
_writeResponseagainst writing to already-closed HTTP sockets. - Banner ASCII padding — Handled high route counts safely with
max(0, ...).
2.0.0 #
Added #
QuadrantRouter— prefix-mounted route grouping; organise large apps without repeating path prefixes. Supports router-level middlewares applied to all routes in the group.WebSocketGroup— built-in broadcast/room utility.add(),remove(),broadcast(),broadcastJson(),closeAll()with lazy cleanup of stale connections.WebSocketContext.isOpen/isClosed— check connection state before sending to avoid throwing on a closed socket.WebSocketContext.sendJson()— convenience JSON-encode-and-send shortcut.- Wildcard route segments —
Route.get(path: '/files/*', ...)captures the full remaining path asreq.params['*']. Response.redirect()— 301/302/307/308 redirect withlocationheader.Response.text()— plain-text response withcontent-type: text/plain.Response.html()— HTML response withcontent-type: text/html.Response.conflict()— 409 Conflict.Response.unprocessableEntity()— 422 Unprocessable Entity.- Typed query helpers on
Request—queryString(),queryInt(),queryDouble(),queryBool()with default value support. Request.bodyAsMap/bodyAsList— safe typed casts for the parsed body.app.close()— graceful shutdown that waits for in-flight requests.docsLocalOnlyparameter — control whether/quadrant_docsis loopback-only (default:true).logger()output sink — injectable log destination:logger(output: myLogger.info).cors()named parameters —methodsandallowedHeadersnow configurable.- Startup banner — printed when
docs: trueshowing host, route counts, and docs URL. - Unit test suite (
test/quadrant_server_test.dart) covering router, response factories, QuadrantRouter, and middleware. analysis_options.yamlwithlints/recommendedand strict type-checking.
Fixed #
RequestHolderhash collision — replacedhashCode-basedMapkey withExpando<Object>keyed on theHttpRequestobject itself. Eliminates body cross-contamination under concurrent load.bodyParsersilently dropped JSON array bodies —req.bodyis nowdynamicand accepts top-levelListpayloads.- Plain-string responses lacked
Content-Type— all string response bodies now carrycontent-type: text/plain; charset=utf-8. - WebSocket upgrade errors silently swallowed — upgrade failures now delegate to
onErroror print a warning. /quadrant_docsaccessible from external IPs — loopback guard is now active by default (docsLocalOnly: true).internalServerErrorleaked raw exception messages — default error handler now returns a generic safe message; users opt in to detail viaonError.
Improved #
cors()now setsVary: Originwhenorigin != '*'for correct CDN/proxy caching behaviour.WebSocketContext.send()is now a no-op when the socket is closed instead of throwing.
1.2.0 #
Added #
- WebSocket support — declarative
WebSocketRoutewith four named callbacks (onStart,onMessage,onClose,onError) WebSocketContext— per-connection context withsend(),close(), and access to the original upgrade request (params, query, headers)webSocketRoutesparameter onQuadrantServer— declare WS endpoints alongside HTTP routes- Middleware support for WebSocket — route-level middlewares run before upgrade; rejection prevents the upgrade entirely
- Path parameters on WS routes —
/ws/chat/:roomIdextracts params just like HTTP routes MatchedWebSocketsealed type — router distinguishes WS upgrades from HTTP requests at the type level- Docs: WebSocket section —
/quadrant_docsnow renders WS endpoints in a table below Swagger UI
Improved #
- Router —
match()acceptsisUpgradeRequestflag; WS upgrades never fall through to HTTP GET routes
1.1.1 #
Added #
/quadrant_docsendpoint — opt-in interactive API explorer powered by Swagger UI (CDN-loaded), enabled viadocs: true- 405 Method Not Allowed — router now distinguishes "path exists, wrong method" from "path not found", returns proper
Allowheader - HEAD request support — HEAD automatically falls back to GET handlers per HTTP spec
- Trailing slash normalization —
/users/and/usersnow match the same route - URI-decoded path params —
%20and other encoded characters decoded automatically Route.paramNamesgetter — extracts parameter names from path pattern for introspection
Improved #
- Router performance — routes grouped by HTTP method for faster lookup (O(n/m) vs O(n))
- Method normalization —
'get'and'GET'both resolve correctly
Security #
/quadrant_docslocked to loopback addresses only — external IPs always get 404- Docs route excluded from generated OpenAPI spec (no self-referencing)
1.0.0 #
- Initial release
- Core framework:
QuadrantServer,Request,Response,Route,Router - Middleware system with global and route-level support
- Built-in middlewares:
cors(),logger(),bodyParser() - Path parameter extraction (
:paramsegments) - Immutable response pattern with static constructors
- Zero external dependencies — built entirely on
dart:io