Answer the formatter's common Accept headers from a table built once - #2924
Closed
ericproulx wants to merge 1 commit into
Closed
ericproulx wants to merge 1 commit into
ericproulx wants to merge 1 commit into
Conversation
An API that pins no `format` negotiates one from the Accept header on every request that sends one, through `Rack::Utils.best_q_match`: about 1.7 µs for `Accept: application/json`. The answer depends only on the header and `mime_types`, which is fixed once the middleware is built. `FormatForAcceptCache` works it out once for every registered media type, `*/*` and no Accept header, and `format_from_header` looks the header up there. Any other header misses and is negotiated as before, and only registered media types are keys, so client input cannot grow a table. Each entry is computed by the same function the full path runs, now a class method so the cache can call it, so a hit answers exactly what the full path would. The keys are valid strings that scrubbing leaves alone, so only a miss scrubs the header. Every endpoint builds its own formatter, so the table is shared per `mime_types`, which `Grape::ContentTypes` already shares per content-type registry. An API that pins a format never negotiates from the header and builds no table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
perf/formatter-accept-table
branch
from
September 10, 2026 19:18
7f1b86f to
921e21f
Compare
Danger ReportNo issues found. |
Danger ReportErrors
Markdowns* [#XXXX](https://github.com/ruby-grape/grape/pull/XXXX): Answer the formatter's common Accept headers from a table built once instead of negotiating them per request - [@ericproulx](https://github.com/ericproulx).
does not include a pull request link |
5 tasks
Contributor
Author
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
format(content negotiation, e.g.default_format :json) negotiated one from the Accept header on every request that sent one, throughRack::Utils.best_q_match: about 1.7 µs forAccept: application/json. The answer depends only on the header and the middleware'smime_types.Formatter::FormatForAcceptCachecomputes it once for every registered media type,*/*and no Accept header, andformat_from_headerlooks the header up. Any other header misses and is negotiated as before. Only registered media types are keys, so client input cannot grow a table.mime_types, whichGrape::ContentTypesalready shares per content-type registry, since every endpoint builds its own formatter. An API that pins aformatnever negotiates from the header and builds no table.Formatter.format_for_accept(@api private) so the cache and the miss path run the same function. The keys are valid strings that scrubbing leaves alone, so only a miss scrubs the header.Benchmarks
Median of 7 interleaved subprocess rounds against master, Ruby 4.0.6:
Accept: application/jsonAccept: */*Boot, 500 negotiating endpoints: retained memory and compile time unchanged.
Behaviour
Byte-identical to master over a 525-case matrix: 7 APIs (defaults,
default_format, a customtext/csvtype, a mixed-case registered type, a registeredapplication/*, a parameterizedapplication/json; charset=utf-8, a pinned format) × 25 Accept values (a browser Accept, axios's default, q-value lists, other casings, invalid bytes, binary-encoded headers) × 3 paths (plain,.jsonextension,?format=xml).Test plan
:txtfor every key fails 7; skipping the scrub on a miss fails 1.🤖 Generated with Claude Code