Skip to content

[Cookies] Refactor for Allocation Improvements, RFC 6265bis Compliance, and SPA Support#777

Open
PingoLee wants to merge 2 commits into
GenieFramework:mainfrom
PingoLee:perf/optimize-cookies
Open

[Cookies] Refactor for Allocation Improvements, RFC 6265bis Compliance, and SPA Support#777
PingoLee wants to merge 2 commits into
GenieFramework:mainfrom
PingoLee:perf/optimize-cookies

Conversation

@PingoLee

Copy link
Copy Markdown
Contributor

Description

This PR performs a major refactor of the Genie.Cookies module to improve performance, strictness, and compliance with modern browser standards (RFC 6265bis), specifically targeting better support for Single Page Applications (SPAs) like Quasar.

Key Changes

  1. Zero-Allocation Retrieval: Replaced split and Dict creation in critical hot paths with SubString views and iterator-based parsing (eachsplit on Julia 1.9+). This significantly reduces GC pressure when reading cookies on high-traffic endpoints.

  2. SPA & CORS Friendly: Implemented automatic enforcement of Secure=true when SameSite=None is detected. This fixes common issues where modern browsers (Chrome/Edge) reject cross-site cookies during SPA development/production.

  3. Strict Type Safety: The get function now strictly throws ArgumentError if a cookie exists but cannot be parsed to the requested type (e.g., requesting an Int but receiving "undefined"). This prevents silent failures and helps detect frontend bugs or tampering.

  4. Optimized Configuration: Introduced load_cookie_settings! in the bootstrap phase to pre-validate and normalize cookie configurations (converting Strings to Symbols/Enums once at startup), rather than per-request.

  5. Enhanced Attribute Support: Added support for legacy CamelCase attributes (Path, HttpOnly, MaxAge, SameSite) with automatic normalization. Implemented logout pattern detection (max_age=0expires=1970-01-01) for proper browser cookie deletion.

  6. Technical Detail: Replaced split(header, ";") which allocates arrays of strings with eachsplit (iterators) and SubString views. Parsing a cookie header now performs 0 allocations in the hot path until the specific key is found and decrypted.

Backward Compatibility (Non-Breaking)

This PR is strictly non-breaking.

  • A new field cookie_defaults was added to Genie.Configuration.Settings initialized as nothing.
  • The logic explicitly checks for nothing. If no defaults are provided (current behavior), the system falls back to the exact previous behavior (empty Dict).
  • Existing applications will run without modification.

Testing

  • Comprehensive test suite with 1970+ lines covering:

    • Request/Response cookie retrieval (encrypted and plaintext)
    • Edge cases (malformed cookies, prefix matching, size limits)
    • Config validation and attribute normalization
    • GenieSession.jl integration (persistence, Flash messages)
    • File-based session storage compatibility
    • SPA/CORS patterns (auto-secure enforcement)
  • Verified integration with GenieSession through extensive unit tests
    demonstrating session creation, persistence, and attribute handling.

Support & Maintenance

I am fully committed to maintaining this PR. While extensive testing has been performed to ensure backward compatibility, I am ready to promptly address any unforeseen regressions or edge cases that may arise.

Future Plans

If this PR is accepted, I plan to submit follow-up PRs to GenieSession.jl and
GenieAuthentication.jl to leverage these optimizations, ensuring the session ID
retrieval benefits from the zero-allocation parsing.

Detailed Changes

1. Simplified Response Cookie Retrieval (get)

Before: Eagerly created a full HTTPUtils.Dict and checked both "Set-Cookie" and "set-cookie" headers
After: Direct delegation to nullablevalue() which handles header lookup internally
Benefit: Eliminates unnecessary Dict allocation on every request

2. Dispatcher Pattern for nullablevalue() (Request vs Response)

Design: Split nullablevalue() into two specialized dispatchers:

  • nullablevalue(payload::HTTP.Request, ...) - Looks for "Cookie" header (client→server)
  • nullablevalue(payload::HTTP.Response, ...) - Looks for "Set-Cookie" header (server→client)

Why separate?

  • Different header formats: Request uses Cookie: key1=val1; key2=val2 vs Response uses Set-Cookie: key=val; Path=/; ...
  • Avoids runtime type checking inside the function body
  • Allows each dispatcher to directly access the correct header without string comparisons
  • Both delegate to the core string-parsing logic for the actual zero-copy parsing

Benefit: Type-directed dispatch at compile-time, zero runtime overhead determining which header to search

3. Configuration Pre-Validation (load_cookie_settings!())

New function: Validates and normalizes cookie config at startup, not per-request
Benefit: Moves validation overhead from hot path to initialization

4. Type-Generic Parsing with AbstractString

Design Choice: nullablevalue(cookie_header::AbstractString, key::Symbol, ...)

Why AbstractString (not String)?

Julia's multiple dispatch system compiles specialized code for each concrete type:

  • When eachsplit() returns a SubString view, the function receives that view directly (zero copy)
  • The compiled code works efficiently with SubString without conversion
  • If forced to String, it would allocate and copy the entire substring

Inline Comments for Code Review

The set!() and nullablevalue() functions include detailed comments explaining design decisions and optimization rationale to facilitate code review. These comments can be removed after review if preferred by the team.

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