From c2b0cf59b29d9d819f2a95c9119bafb609d64225 Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Mon, 1 Jun 2026 23:57:04 +0200 Subject: [PATCH] Build the cookie jar only on use and drop its deferral Proc Two related steps that make the response-cookie path lazy at the request boundary: - `build_response_cookies` runs on every request and went through `cookies` -> `request.cookies`, materializing the `Grape::Cookies` jar even when the handler never touched a cookie. Gate it on a new `Grape::Request#cookies?` predicate (true only once the jar exists), so a cookie-free request allocates no jar at all. - With the jar now built only when a cookie is actually read or written, its `-> { rack_cookies }` deferral Proc no longer earns its keep: any access immediately forces the parse, and a write (`[]=`) always did. Parse `rack_cookies` eagerly in the constructor and replace the `is_a?(Proc)` lazy reader with a plain `attr_reader`, dropping the closure allocation and the per-access branch. The `ActiveSupport::HashWithIndifferentAccess` wrapping is unchanged -- it backs Grape's string/symbol-indifferent cookie access. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 + lib/grape/cookies.rb | 8 ++------ lib/grape/endpoint.rb | 2 ++ lib/grape/request.rb | 10 +++++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a93d89c0..3ac8669d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ * [#2754](https://github.com/ruby-grape/grape/pull/2754): Merge routing args in place in `Router#process_route` instead of allocating a new Hash via `merge` - [@ericproulx](https://github.com/ericproulx). * [#2753](https://github.com/ruby-grape/grape/pull/2753): Lazy-allocate `Grape::Validations::ParamScopeTracker`'s identity-keyed hashes so validating requests that never use the index / qualifying-params trackers allocate no hash - [@ericproulx](https://github.com/ericproulx). * [#2752](https://github.com/ruby-grape/grape/pull/2752): Skip per-request `ActiveSupport::Notifications` payload and dispatch when no subscriber is listening, via private `instrument_` guards on `Endpoint`/`Middleware::Formatter` - [@ericproulx](https://github.com/ericproulx). +* [#2757](https://github.com/ruby-grape/grape/pull/2757): Build the `Grape::Cookies` jar only when a cookie is read or written (via a new `Grape::Request#cookies?` predicate gating response-cookie flushing), and drop the jar's now-redundant lazy-parse `Proc` - [@ericproulx](https://github.com/ericproulx). * Your contribution here. #### Fixes diff --git a/lib/grape/cookies.rb b/lib/grape/cookies.rb index 1b0533c95..877eecd5a 100644 --- a/lib/grape/cookies.rb +++ b/lib/grape/cookies.rb @@ -13,7 +13,7 @@ class Cookies def_delegators :cookies, :[], :each def initialize(rack_cookies) - @cookies = rack_cookies + @cookies = ActiveSupport::HashWithIndifferentAccess.new(rack_cookies) @send_cookies = nil end @@ -37,11 +37,7 @@ def delete(name, **opts) private - def cookies - return @cookies unless @cookies.is_a?(Proc) - - @cookies = ActiveSupport::HashWithIndifferentAccess.new(@cookies.call) - end + attr_reader :cookies def send_cookies @send_cookies ||= Set.new diff --git a/lib/grape/endpoint.rb b/lib/grape/endpoint.rb index 2f552ee0b..95614ef2a 100644 --- a/lib/grape/endpoint.rb +++ b/lib/grape/endpoint.rb @@ -385,6 +385,8 @@ def build_helpers end def build_response_cookies + return unless request.cookies? + response_cookies do |name, value| cookie_value = value.is_a?(Hash) ? value : { value: } Rack::Utils.set_cookie_header! header, name, cookie_value diff --git a/lib/grape/request.rb b/lib/grape/request.rb index 9cbc14ac5..12a0fca0d 100644 --- a/lib/grape/request.rb +++ b/lib/grape/request.rb @@ -153,7 +153,15 @@ def headers end def cookies - @cookies ||= Grape::Cookies.new(-> { rack_cookies }) + @cookies ||= Grape::Cookies.new(rack_cookies) + end + + # True once the cookie jar has been materialized (a cookie was read or + # written this request). Lets the endpoint skip response-cookie flushing, + # and the Grape::Cookies allocation it triggers, when no cookie was + # touched on the request. + def cookies? + !@cookies.nil? end private