8000 🔥 feat: session: multi-store requests and pluggable codec by gaby · Pull Request #4815 · gofiber/fiber · GitHub
[go: up one dir, main page]

Skip to content

🔥 feat: session: multi-store requests and pluggable codec - #4815

Draft
gaby wants to merge 1 commit into
mainfrom
ccr-ac6228da-p45qep
Draft

gaby wants to merge 1 commit into
mainfrom
ccr-ac6228da-p45qep

Conversation

@gaby
@gaby gaby commented Oct 29, 2026 •
Copy link
Copy Markdown
Member

Description

Six additive, non-breaking improvements to middleware/session; every new option is off by default. With the default configuration a differential run against main gives the same responses and storage calls, apart from the cases under "Behavior that differs from main".

  1. Several sessions per request. A store takes the IDs Store.Get issued only from stores that read the same source (cookie, header, query parameter...), so a second store no longer adopts another store's ID. Store.FromContext(c) returns one store's middleware.
  2. Config.SkipEmptySessions keeps a new session without data out of the storage and the response (saveUninitialized: false in express-session).
  3. Config.StorageKeyPrefix and Config.HashStorageKeys keep applications that share a storage apart and store each session under the SHA-256 digest of its ID.
  4. Hardening. A save keeps the record and the cookie no longer than the time left before AbsoluteTimeout; warnings for __Host-/__Secure- cookies that browsers reject and for store options set on the wrong Config; a typed-nil Storage panics in NewStore.
  5. Ergonomics. session.GetAs[V], IdleTimeout() and AbsoluteExpiry().
  6. Pluggable codec. Config.Codec stores each value encoded on its own: a request decodes only what it reads (GetInto, GetAs) and a save encodes only what was set. session.JSONCodec{} and session.NewCodec(name, marshal, unmarshal). Existing sessions stay gob until they end.

Based on main at cf50ad3. Lifecycle fixes beyond the five changes asked for stay in this change (differences 4, 6 and 7); their tests, in middleware_test.go and store_test.go, can be reviewed or split out separately. About 10,400 added lines: 2,800 production, 7,400 tests, 240 docs.

Fixes: no open issue.

Changes introduced

  • Benchmarks: store_test.go, middleware_test.go, session_test.go, codec_test.go.
  • Documentation Update: docs/middleware/session.md, docs/middleware/csrf.md.
  • Changelog/What's New: docs/whats_new.md.
  • Migration Guide: no signature change; every option defaults to the old behavior.
  • API Alignment with Express: SkipEmptySessions mirrors saveUninitialized: false.
  • API Longevity: additions only; Store holds atomics now, so go vet flags copying one by value.
  • Examples: in the docs.

Behavior that differs from main

Details in docs/whats_new.md.

  1. Stores over different cookies no longer adopt each other's cached ID.
  2. A save keeps record and cookie for the time left before AbsoluteTimeout: the first cookie says Max-Age=1799 where main says 1800 when it equals IdleTimeout.
  3. A typed-nil Storage panics in NewStore, not on the first request.
  4. An abandoned request's middleware and session are not recycled; after a timeout answer a handler's own Save writes the record and sets no cookie.
  5. A store without a Codec that finds a codec record starts a new session and logs a warning, where main answers 500.
  6. A store without AbsoluteTimeout honours a stored deadline; once passed, GetByID no longer finds the session.
  7. After a panic unwinds the middleware, a Save from the recovering middleware saves, and a goroutine can no longer write its cookie into another request.
  8. store.Get after the middleware has finished reads the ID from the request again, or finds the session an earlier store.Get saved.
  9. Stores are told apart by every source they read, so a link ?sid=X no longer makes the cookie stores behind a query store serve X.
  10. Once the inner of two stacked session middleware has finished, session.FromContext(c) returns the outer one, not nil.
  11. With several session middleware in a request, csrf keeps its token in the session of the store it is given as Session, not of the innermost middleware; with one middleware, or when that store has none, nothing changes.

Performance

Against main (cf50ad3), the same benchmark code in both binaries; allocations from -benchmem (5,000 iterations), instructions per iteration from valgrind callgrind (the cost of the benchmark body over 20,000 iterations, GC off, one P, mean of 3 to 6 runs).

benchmark instructions vs main allocs/op main -> branch B/op main -> branch
middleware, returning visitor -1327 (-0.89%) 198 -> 195 9152 -> 8973
middleware, PassLocalsToContext -438 (-0.29%) 201 -> 199 9505 -> 9182
Store.Get+Save, ID cached +158 (+0.11%) 195 -> 194 9033 -> 8955
Store.Get+Save, own storage -308 (-0.22%) 195 -> 194 9033 -> 8953
Store.Get+Save, returning visitor +167 (+0.13%) 182 -> 181 8182 -> 8103
Store.Get+Save, new visitor -101 (-0.35%) 26 -> 26 1723 -> 1651

Runs of one binary differ by up to 1,000 instructions, so only the returning-visitor middleware differs from main beyond that spread: it is lower because the branch drops the per-request Config copy and a Go context wrapped twice. Bytes per operation are lower in every row, and allocations in every row but the new visitor. Without a Codec, Session.Get and Middleware.Get cost what they do on main to within 2% (344 instructions against 345, and 404 against 397) and Set 4 and 7 more (callgrind, mean of three runs). HashStorageKeys costs about 0.1 µs; SkipEmptySessions makes a visitor without a cookie cheaper (2 allocations instead of 19).

With a Codec (Benchmark_Codec_Request, a signed-in user with five values):

request gob JSONCodec
reads one value 281 allocations, 12.9 KB 15 allocations, 1.2 KB
reads all five values 320 allocations, 15.8 KB 43 allocations, 2.6 KB
changes one value 320 allocations, 15.8 KB 16 allocations, 1.3 KB

A codec decodes on every read and encodes on every set, so it loses to gob when a request reads the same struct many times (2.5 times as long at 20 reads), sets one key 1000 times (10 times) or handles a 100 KiB string (5 times); the docs say so under "When gob is the better choice".

Verification

  • go test ./... -race -shuffle=on passes (session and csrf also five times in a row, and as 32-bit binaries); go vet is clean for five platforms; lint (with gosec) and markdownlint report nothing. make audit fails on main too, for the same 38 advisories in the go1.26.0 standard library.
  • A differential run against main (79 scenario groups, 677 requests) gives the same responses and storage calls, apart from difference 2. The four fuzz targets ran 30 s each without a finding.
  • The rule main added, that a request which only read does not store its snapshot over a change another request stored meanwhile, and that its Regenerate carries that change, holds for a store with a Codec as well (Test_Codec_UnchangedRequestKeepsConcurrentChange); 109 hand-written mutants of the new code are all caught by the tests.
  • Separate review sessions attacked the lifecycle and locks, the record parser and codec path, and session identity and storage keys; none found anything a remote attacker could use, and the defects they did find are fixed with tests (differences 6 to 9).
  • Written with Claude Code and reviewed in separate AI sessions; none of this replaces a maintainer's review. To repeat: go test ./middleware/session ./middleware/csrf -race -count=1 -shuffle=on.

Known limitations

All of them are in docs/middleware/session.md; the ones to know before adopting:

  • An unhashed StorageKeyPrefix that, followed by session:, starts another store's prefix lets a client address that store's records through the shorter one, as on main: use HashStorageKeys.
  • Rolling out a Codec: a release without this feature answers a codec record with a 500; deploy one that has it with the option off everywhere first.
  • csrf cannot use a session store with a codec (its token key is not a string): csrf.New panics for Config.Session.

Type of change

  • New feature (non-breaking change which adds functionality)
  • Enhancement (improvement to existing features and functionality)
  • Documentation update (changes to documentation)
  • Performance improvement (non-breaking change which improves efficiency)
  • Code consistency (non-breaking change which improves code reliability and robustness)

Checklist

  • Followed the inspiration of the Express.js framework for new functionalities, making them similar in usage.
  • Conducted a self-review of the code and provided comments for complex or critical parts.
  • Updated the documentation in the /docs/ directory for Fiber's documentation.
  • Added or updated unit tests to validate the effectiveness of the changes or new features.
  • Ensured that new and existing unit tests pass locally with the changes.
  • Verified that any new dependencies are essential and have been agreed upon by the maintainers/community: none were added.
  • Aimed for optimal performance with minimal allocations in the new code.
  • Provided benchmarks for the new code to analyze and improve upon.

🤖 Maintainers: /generate re-runs the code generators on this branch

🤖 Generated with Claude Code

https://claude.ai/code/session_01Wv2sey8ZLZai81oADq52Re

@coderabbitai
coderabbitai Bot commented Oct 29, 2026 •
Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@codecov
codecov Bot commented Oct 29, 2026 •
Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.14299% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 96.19%. Comparing base (cf50ad3) to head (9e89f66).
⚠️ Report is 19 commits behind head on main.

Files with missing lines Patch % Lines
middleware/session/store.go 96.95% 4 Missing and 4 partials ⚠️
middleware/session/codec.go 98.47% 2 Missing and 2 partials ⚠️
middleware/session/data.go 96.89% 2 Missing and 2 partials ⚠️
middleware/session/session.go 97.10% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4815      +/-   ##
==========================================
+ Coverage   96.06%   96.19%   +0.13%     
==========================================
  Files         156      157       +1     
  Lines       20016    20856     +840     
==========================================
+ Hits        19228    20063     +835     
- Misses        477      478       +1     
- Partials      311      315       +4     
Flag Coverage Δ
unittests 96.19% <98.14%> (+0.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@gaby
gaby commented Oct 29, 2026
Copy link
Copy Markdown
Member Author

Keeping draft until other PR's are merged.

@gaby
gaby force-pushed the ccr-ac6228da-p45qep branch from 5646fdf to f54397c Compare October 29, 2026 14:48
@gaby gaby changed the title 🔥 feat: session: multi-store requests, SkipEmptySessions, key prefix/hash, deadline cap, GetAs 🔥 feat: session: multi-store requests, SkipEmptySessions, key prefix/hash, deadline cap, GetAs, pluggable codec Oct 29, 2026
@github-actions
github-actions Bot commented Oct 30, 2026 •
Copy link
Copy Markdown
Contributor

✅ No significant benchmark change.

c58244e vs main@0bc83ea · 2212/2284 results compared · 72 new, 0 gone · retest: 1 reported re-checked · noise-aware thresholds · full results

@gaby
gaby force-pushed the ccr-ac6228da-p45qep branch 4 times, most recently from e70e243 to 12ba319 Compare November 2, 2026 15:28
@gaby
gaby commented Nov 2, 2026
Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector
chatgpt-codex-connector Bot commented Nov 2, 2026 •
Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-02T15:36:53.538783Z 12ba319 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 12ba319acb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@gaby
gaby force-pushed the ccr-ac6228da-p45qep branch 2 times, most recently from 519c02e to a49c81c Compare November 5, 2026 01:05
@gaby gaby changed the title 🔥 feat: session: multi-store requests, SkipEmptySessions, key prefix/hash, deadline cap, GetAs, pluggable codec 🔥 feat: session: multi-store requests and pluggable codec Nov 5, 2026
@gaby
gaby force-pushed the ccr-ac6228da-p45qep branch 5 times, most recently from 24463bf to 9b6fa29 Compare November 6, 2026 15:02
…hash, deadline cap, GetAs, pluggable codec

Six additive, non-breaking changes to middleware/session. Every new option is
off by default; with the default configuration the responses and the storage
calls are those of main, apart from the cases listed below.

- Several sessions per request. A store takes the IDs Store.Get issued only
  from stores that read the same source (cookie, header, query parameter...),
  so a second store no longer adopts another store's ID. Store.FromContext(c)
  returns one store's middleware.
- Config.SkipEmptySessions keeps a new session without data out of the storage
  and the response (saveUninitialized: false in express-session).
- Config.StorageKeyPrefix and Config.HashStorageKeys keep applications that
  share a storage apart and store each session under the SHA-256 digest of its
  ID.
- Hardening. A save keeps the record and the cookie no longer than the time
  left before AbsoluteTimeout; warnings for __Host-/__Secure- cookies that
  browsers reject and for store options set on the wrong Config; a typed-nil
  Storage panics in NewStore.
- Ergonomics. session.GetAs[V], IdleTimeout() and AbsoluteExpiry().
- Pluggable codec. Config.Codec stores each value encoded on its own, so a
  request decodes only what it reads (GetInto, GetAs) and a save encodes only
  what was set. session.JSONCodec{} and session.NewCodec(name, marshal,
  unmarshal). Existing sessions stay gob until they end. csrf keeps its token
  under a key that is not a string, so csrf.New panics for a session store
  with a codec.

Behavior that differs from main (details in docs/whats_new.md):

1. Stores over different cookies no longer adopt each other's cached ID.
2. A save keeps record and cookie for the time left before AbsoluteTimeout: the
   first cookie says Max-Age=1799 where main says 1800 when it equals
   IdleTimeout.
3. A typed-nil Storage panics in NewStore, not on the first request.
4. An abandoned request's middleware and session are not recycled; after a
   timeout answer a handler's own Save writes the record and sets no cookie.
5. A store without a Codec that finds a codec record starts a new session and
   logs a warning, where main answers 500.
6. A store without AbsoluteTimeout honours a stored deadline; once passed,
   GetByID no longer finds the session.
7. After a panic unwinds the middleware, a Save from the recovering middleware
   saves, and a goroutine can no longer write its cookie into another request.
8. store.Get after the middleware has finished reads the ID from the request
   again, or finds the session an earlier store.Get saved.
9. Stores are told apart by every source they read, so a link ?sid=X no longer
   makes the cookie stores behind a query store serve X.
10. Once the inner of two stacked session middleware has finished,
    session.FromContext(c) returns the outer one, not nil.
11. With several session middleware in a request, csrf keeps its token in the
    session of the store it is given as Session, not of the innermost
    middleware; with one middleware, or when that store has none, nothing
    changes.

Without a Codec, Session.Get and Middleware.Get cost what they do on main, and
the session benchmarks allocate the same or less. Docs: docs/middleware/session.md,
docs/middleware/csrf.md and docs/whats_new.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wv2sey8ZLZai81oADq52Re
@gaby
gaby force-pushed the ccr-ac6228da-p45qep branch from 9b6fa29 to 9e89f66 Compare November 6, 2026 15:23

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants

0