Answer the header versioner's common Accept headers from a table built once - #2922
Closed
ericproulx wants to merge 1 commit into
Closed
Answer the header versioner's common Accept headers from a table built once#2922ericproulx wants to merge 1 commit into
ericproulx wants to merge 1 commit into
Conversation
…t once The header versioner ran Rack's q-value match (`Rack::Utils.best_q_match`, which splits both strings inside `Rack::Mime.match?` for every candidate) and a media-type parse on every request: about 3.6 µs of a 12.4 µs request. Its answer depends only on the Accept header and the middleware's `available_media_types`, which is fixed once the middleware is built. `MediaTypeForAcceptCache` works that answer out once for every declared media type, for `*/*` (what curl, Net::HTTP and most HTTP libraries send) and for no Accept header at all, and the versioner looks the header up there. Any other header, such as a q-value list or another casing, misses and takes the full path unchanged. Only declared media types are keys, so client input cannot grow a table, and each entry is computed by the real function, so a hit answers exactly what the full path would. Every endpoint builds its own versioner, so the table is shared per list through `Grape::Util::Cache`, as `ContentTypes::MimeTypesCache` already is. Built per instance it cost 3.5 MB and 120 ms of compile time on a 500-endpoint API; shared, 16 KB and 1 ms. Handing one parsed media type to many requests means the strings it gives out must not be alterable in place, so `Grape::Util::MediaType` is now immutable, strings included, on the full path too so both paths agree. `#initialize` copies its arguments rather than freezing the caller's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
perf/header-versioner-accept-table
branch
from
September 10, 2026 19:18
c3e7660 to
a8b2942
Compare
Danger ReportNo issues found. |
5 tasks
Contributor
Author
This was referenced Sep 11, 2026
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
version ..., using: :headerranRack::Utils.best_q_match(which splits both strings insideRack::Mime.match?for every candidate) and aMediaType.parseon every request: about 3.6 µs of a 12.4 µs request. Its answer depends only on the Accept header and the middleware'savailable_media_types, which is fixed once the middleware is built.Versioner::Header::MediaTypeForAcceptCachecomputes that answer once for every declared media type, for*/*(what curl, Net::HTTP and most HTTP libraries send) and for no Accept header, and the versioner looks the header up. Any other header (q-value lists, another casing) misses and takes the unchanged full path. Only declared media types are keys, so client input cannot grow a table, and each entry is computed by the real function, so a hit answers exactly what the full path would.Grape::Util::Cache, asContentTypes::MimeTypesCachealready is.Grape::Util::MediaTypeis now immutable, strings included.Contract change
The strings the header versioner writes into the env (
api.type,api.subtype,api.vendor,api.version,api.format) are now frozen, since requests sending the same Accept header share one parsed media type. They are frozen on the full path too, so behaviour does not depend on which Accept header came in. Code that altered one of them in place now raisesFrozenError; UPGRADING has an entry.MediaType#initializecopies its arguments rather than freezing the caller's Strings.Benchmarks
Median of 7 interleaved subprocess rounds against master, Ruby 4.0.6:
Accept: application/vnd.acme-v1+jsonAccept: */*Boot, 500 header-versioned endpoints: +16 KB retained, +1 ms compile. A table built per instance cost +3.5 MB and +120 ms, which is why it is shared.
Behaviour
Byte-identical to master over a 150-case matrix, apart from the frozen strings: 5 APIs (one version, two versions,
strict,cascade: false, dotted vendor and version) × 30 Accept values, including q-value lists, wildcards, other casings, surrounding whitespace, parameters, invalid bytes and binary-encoded headers.Test plan
🤖 Generated with Claude Code