Skip to content

Performance optimization - #1

Merged
kartikey321 merged 5 commits into
mainfrom
performance-optimization
Mar 15, 2026
Merged

kartikey321 merged 5 commits into
mainfrom
performance-optimization

Conversation

@kartikey321

@kartikey321 kartikey321 commented Mar 10, 2026 •

Copy link
Copy Markdown
Owner

Performance, Security & Quality — Full Overhaul

Benchmark

Before After
Fletch RPS ~28,500 44,277
vs dart:io −42% −9.5%
Ranking Last among Dart frameworks One of the top Dart frameworks

p50 latency dropped from 0.27 ms → 0.17 ms. Memory held steady at ~18.4 MB.


Performance Changes

Lazy ID generation (biggest single regression fix)

Session IDs and request IDs were previously generated eagerly on every request using Random.secure() — 36 bytes of OS entropy consumed even for routes that never touch sessions. Benchmark routes paid ~2.9 µs per request for IDs that were immediately thrown away.

Both are now generated lazily — only on first access. Routes that skip req.session and req.requestId pay exactly zero.

// Session ID — generated on first req.session access only
_sessionId ??= _generateSessionId();

// Request ID — generated on first req.requestId access only
String get requestId => _requestId ??= _generateRequestId();

Nullable requestTimeout — remove per-request Timer

requestTimeout: null disables the 30s timeout entirely. This removes a Timer + Future + two closure allocations per request — the single largest throughput improvement (~7k RPS).

Environments with external timeout enforcement (nginx, load balancers) should use null in production.

Lazy session object + sessionTouched gate

The Session object, all store I/O, and Set-Cookie emission are now gated behind a single sessionTouched boolean. Routes like /health and /api/echo pay zero session overhead.

Zero-middleware fast path

When no global or route-level middleware is registered, wrapWithMiddleware short-circuits directly to the handler — no closure or index variable allocations.

Fused JSON encoder

res.json() now uses a static JsonUtf8Encoder that encodes directly to Uint8List in one step, removing the intermediate String allocation per JSON response.

Lazy maps everywhere

  • req.query — Uri.queryParameters only called on first access
  • res.headers — LinkedHashMap only allocated when a custom header is set
  • RouteMatch.pathParams — static routes share a single const {} map

Router hot-path improvements

  • RadixRouter: static RegExp fields, normalization removed from findRoute, List processed allocation eliminated
  • ListRouter: tryMatch() does one regex pass instead of two; prefix boundary fix prevents /api matching /apix/...

Security Fixes

Error response leaks internal details HIGH

Before: Any unhandled exception sent error.toString() to the client — leaking DB connection strings, file paths, library internals.

After: Generic "Internal Server Error" by default. Opt in to full detail with Fletch(debug: true) for local development only.

Session fixation HIGH

Before: No way to change session ID after login — attacker could fix a known ID before authentication.

After: await req.session.regenerate() — destroys the old session record, generates a new cryptographically-random ID, and emits a new Set-Cookie automatically.

app.post('/login', (req, res) async {
  if (await validate(req)) {
    await req.session.regenerate(); // call before writing user data
    req.session['userId'] = user.id;
  }
});

Session IDs were predictable counters HIGH

Before: ses_<microsecond-prefix>_<n> — sequential, enumerable.

After: ses_<32-char base64url> — 192 bits from Random.secure(), generated lazily.

Cookie prefix-confusion attack HIGH

Before: indexOf('sessionId=') matched evilSessionId=x and stopped at the first occurrence, missing the real cookie.

After: Split-on-; parser with exact name comparison — correctly skips prefixed cookies and finds the real session cookie regardless of order.

MemorySessionStore — unbounded memory MEDIUM

Before: No size cap — ~86,400 sessions/day at 1 req/sec with no eviction.

After: maxSessions cap (default 10,000) with oldest-first eviction on insert.

Rate limiter ineffective behind reverse proxies MEDIUM

Documented clearly with a keyGenerator example that reads X-Forwarded-For safely, including the trust warning.

Multipart filename path traversal LOW

MultipartFile.filename is attacker-controlled and may contain ../../etc/passwd. New sanitizedFilename extension strips all path components:

final safe = file.sanitizedFilename; // 'avatar.png', never '../secret'

Tests

Before After
Total tests ~170 286
Coverage — 94.9%
Mutation score — 96.7%

New test files

File Coverage
test/integration/tls_test.dart listenSecure() IPv4 binding, v6Only default, client cert default
test/integration/cors_test.dart CORS preflight, origin allowlist, method gating
test/integration/error_handler_test.dart Custom handler, session store resilience
test/integration/rate_limiter_test.dart Limit enforcement, window reset
test/integration/fletch_features_test.dart Full lifecycle: DI, sessions, middleware, errors
test/unit/list_router_test.dart All ListRouter branches including isolated prefix boundary
test/unit/response_test.dart All Response methods
test/unit/coverage_gaps_test.dart Session store, DI, multipart caching, SSE
test/unit/coverage_extension_test.dart Cookie parsing edge cases, sanitizedFilename, regenerate()
test/security/security_test.dart Error redaction, regenerate(), ID entropy, maxSessions eviction, sanitizedFilename

Mutation testing

Ran dart_mutant against all security-critical paths. 96.7% of mutations killed on the targeted files — the only survivor was a default boolean in listenSecure(), now covered by the new TLS integration tests.


CI

ci.yml — runs on every push to main and all PRs

  1. dart analyze --fatal-infos
  2. dart test --coverage
  3. Enforce ≥ 90% line coverage (currently 94.9%)
  4. Upload to Codecov

mutation.yml — weekly scheduled job (Mondays 03:00 UTC)

  • 50% mutation sample, --threshold 75
  • Outputs HTML dashboard, JUnit XML, and AI-optimized markdown report as artifacts
  • Manually triggerable from the Actions tab with configurable sample size and threshold

Breaking Changes

None. All public API is backwards-compatible.

  • debug parameter added to Fletch() — defaults to false
  • maxSessions parameter added to MemorySessionStore() — defaults to 10000
  • session.regenerate() is a new method — no existing code affected
  • MultipartFileExtension.sanitizedFilename is additive

Files Changed

28 files changed, 3,569 insertions(+), 140 deletions(-)

lib/src/models/request.dart              — lazy IDs, secure tokens, cookie parser, regenerate(), sanitizedFilename
lib/src/models/response.dart             — fused JSON encoder, lazy headers
lib/src/models/memory_session_store.dart — maxSessions cap + eviction
lib/src/services/base_container.dart     — sessionTouched gate, debug flag, regeneration cookie
lib/src/services/fletch.dart             — nullable timeout, debug flag, rate limiter docs
lib/src/router/radixRouter/              — static RegExp, hot-path normalization skip, no processed list
lib/src/router/listRouter/               — tryMatch() single-pass, prefix boundary fix
lib/src/router/router_interface.dart     — shared const empty pathParams
.github/workflows/ci.yml                 — new: analyze + test + coverage + Codecov
.github/workflows/mutation.yml           — new: weekly mutation testing
test/integration/tls_test.dart           — new
test/integration/cors_test.dart          — new
test/integration/error_handler_test.dart — new
test/integration/rate_limiter_test.dart  — new
test/integration/fletch_features_test.dart — new
test/unit/list_router_test.dart          — new
test/unit/response_test.dart             — new
test/unit/coverage_gaps_test.dart        — new
test/unit/coverage_extension_test.dart   — new
test/security/security_test.dart         — expanded

Core changes across request lifecycle, routing, and session handling:

- request.dart: lazy query params, lazy Session object + sessionTouched flag,
  preloadSession() bypass, fast inline cookie extraction, sequential IDs
  (replaces UUID), isolate-unique ID prefix, cookie prefix security fix
- response.dart: static JsonUtf8Encoder (fused encoder, avoids String
  intermediate), lazy headers map allocation
- base_container.dart: session I/O gated on sessionTouched (skip load/save/
  Set-Cookie for routes that never touch session), skip load() for new
  sessions, X-Request-Id echoed only when client sends it, zero-middleware
  fast path bypasses closure chain entirely
- fletch.dart: requestTimeout is now Duration? — null disables per-request
  Timer allocation (biggest single gain ~7k RPS)
- router_interface.dart: static const empty map in RouteMatch avoids
  per-request HashMap for no-param routes
- radix_route.dart: static cached RegExp, skip path normalization on hot
  path (dart:io paths are already clean), removed per-call processed list,
  break after static segment match
- route_entry.dart: tryMatch() does method check + regex + param extraction
  in one pass, replacing separate matches() + extractParams() calls

Result: 43,794 RPS (134.8% CPU, 19.3 MB) vs serinus 39,125 / dart_io 48,652
Security fixes:
- Redact internal error details by default (debug: false); expose only
  with debug: true — prevents leaking DB addresses, stack traces, etc.
- Add session.regenerate() to prevent session fixation after login
- MemorySessionStore: cap at maxSessions (default 10k) with oldest-first
  eviction to prevent OOM DoS
- Cookie parser: split-on-semicolon extraction prevents prefix-confusion
  attacks (evilfletch.sid=x;fletch.sid=real now correctly resolves)
- Add MultipartFileExtension.sanitizedFilename stripping path traversal
- Document rate limiter proxy bypass with X-Forwarded-For example

Performance:
- Lazy session ID and request ID generation — Random.secure() tokens now
  generated only on first access; benchmark routes that skip sessions pay
  zero entropy cost (37.8k → 44.3k RPS, #1 among Dart frameworks)

Tests (286 total, 94.9% coverage):
- Security test suite: error redaction, session.regenerate(), sanitizedFilename,
  MemorySessionStore eviction, session ID entropy
- TLS integration tests: listenSecure() IPv4 binding, v6Only default,
  requestClientCertificate default (closes mutation testing gap)
- New test files: cors, error_handler, fletch_features, rate_limiter,
  list_router, response, coverage_gaps, coverage_extension

CI:
- ci.yml: analyze + test + 90% coverage enforcement + Codecov upload
- mutation.yml: weekly dart_mutant run (50% sample, 75% score threshold)
  with HTML/JUnit/AI reports as artifacts
@gitguardian

gitguardian Bot commented Mar 10, 2026 •

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
28248803 Triggered Generic Private Key 100decc packages/fletch/test/integration/tls_test.dart View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

- Remove duplicate debug field from Fletch (inherited from BaseContainer)
- Use super.debug constructor param to satisfy use_super_parameters lint
- Add ignore_for_file for constant_identifier_names on HTTP method constants
- Exclude benchmark/ from analysis_options to prevent dartmark noise
- Remove unused imports in cors_test, coverage_extension_test, coverage_gaps_test
- Remove unused port variable in fletch_features_test
- Replace hardcoded TLS cert/key in tls_test with runtime openssl generation
- Add apps/fletch_bench/drafts/ to .gitignore for local notes
- Remove OPTIMIZATION_CHANGELOG.md from tracked files (moved to drafts/)
@kartikey321
kartikey321 merged commit 014eb38 into main Mar 15, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant