Skip to content

Keep content-type negotiation in place as ensure_content_type! - #2910

Merged
ericproulx merged 1 commit into
masterfrom
perf/ensure-content-type-in-place
Sep 6, 2026
Merged

Keep content-type negotiation in place as ensure_content_type!#2910
ericproulx merged 1 commit into
masterfrom
perf/ensure-content-type-in-place

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

#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 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:

expression obj/call breakdown
headers[K] = v (pre-#2908) 0
headers.merge(K => v) (#2908) 3 2 × Hash at the call site + 1 × Rack::Headers in Rack::Headers#merge
headers.merge(prebuilt) 1 1 × Rack::Headers
headers.dup then []= 1 1 × Rack::Headers

Two 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_response drops the typed_headers local it only needed in order to hold the copy — headers is still never reassigned, which is what #2908 was actually after.

Perf

2000 requests through a formatted endpoint, whole-process allocations:

allocated objects
merge (master) 19906.25 kB 196,000
dup + []= 19281.25 kB 192,000
in place (this PR) 18968.75 kB 190,000

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; the oneof half is untouched and still accurate. Neither shipped — both are unreleased under 4.0.0.

Test plan

  • Full RSpec suite passes locally (2834 examples, 0 failures).
  • RuboCop clean on the changed file (the 21 repo-wide offenses are pre-existing on master).
  • CI green.

🤖 Generated with Claude Code

#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
ericproulx force-pushed the perf/ensure-content-type-in-place branch from 07d59ec to 303cb29 Compare September 6, 2026 11:29
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx requested a review from dblock September 6, 2026 11:30
@ericproulx
ericproulx marked this pull request as ready for review September 6, 2026 11:30
@ericproulx
ericproulx merged commit 251810e into master Sep 6, 2026
69 checks passed
@ericproulx
ericproulx deleted the perf/ensure-content-type-in-place branch September 6, 2026 11:33
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