Keep content-type negotiation in place as ensure_content_type! - #2910
Merged
Conversation
#2908 stopped `Grape::Middleware::Formatter#ensure_content_type` from writing into the headers Hash it was handed, returning `headers.merge(Rack::CONTENT_TYPE => ...)` instead. That copy runs on almost every response, and the inline pair made it cost three allocations rather than one: Ruby builds the one-pair Hash literal, then copies it again converting the implicit keyword Hash into a positional argument for `Hash#merge`, and `Rack::Headers#merge` dups the receiver on top of that. Measured over 2000 requests through a formatted endpoint: merge 19906.25 kB / 196,000 objects in place 18968.75 kB / 190,000 objects Three objects per response on the hot path, for a header the caller is about to send anyway. The negotiation goes back to writing into `headers`, and the method carries a `!` so the mutation is visible at the call site. `build_formatted_response` drops the `typed_headers` local it only needed in order to hold the copy, so `headers` is still never reassigned, which is what #2908 was after. The trade is that a mounted plain Rack app returning a frozen or shared headers Hash is written into again, as it was before #2908. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
perf/ensure-content-type-in-place
branch
from
September 6, 2026 11:29
07d59ec to
303cb29
Compare
Danger ReportNo issues found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#2908 stopped
Grape::Middleware::Formatter#ensure_content_typefrom writing into the headers Hash it was handed, returningheaders.merge(Rack::CONTENT_TYPE => ...)instead. That copy runs on almost every response, and it showed up as a ~11% jump in total allocated memory on a real API profile.The inline pair made it cost three allocations, not one:
headers[K] = v(pre-#2908)headers.merge(K => v)(#2908)Hashat the call site + 1 ×Rack::HeadersinRack::Headers#mergeheaders.merge(prebuilt)Rack::Headersheaders.dupthen[]=Rack::HeadersTwo of the three are pure accident: Ruby builds the one-pair Hash literal, then copies it again converting the implicit keyword Hash into a positional argument for
Hash#merge.This restores the in-place write and renames the method
ensure_content_type!so the mutation is visible at the call site.build_formatted_responsedrops thetyped_headerslocal it only needed in order to hold the copy —headersis still never reassigned, which is what #2908 was actually after.Perf
2000 requests through a formatted endpoint, whole-process allocations:
merge(master)dup+[]=Three objects per response, for a header the caller is about to send anyway.
Trade-off
A mounted plain Rack app that returns a frozen or shared constant headers Hash is written into again, as it was before #2908. No spec covers that, and it was Grape's behaviour for years before #2908, so nothing regresses relative to 3.x — but it is the reason #2908 changed this, so calling it out.
CHANGELOG
#2908's entry claims
ensure_content_type"no longer write[s] into the Hash they were given". That half is no longer true, so this PR corrects that line and drops the claim; theoneofhalf is untouched and still accurate. Neither shipped — both are unreleased under 4.0.0.Test plan
🤖 Generated with Claude Code