Skip to content

Keep each API's content type order when negotiating */* - #3032

Merged
ericproulx merged 1 commit into
masterfrom
fix/content-type-order-cache
Oct 19, 2026
Merged

ericproulx merged 1 commit into
masterfrom
fix/content-type-order-cache

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Every browser's Accept header ends in */*:

Browser Page-load Accept
Chrome, Edge, Opera text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Firefox, Safari text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

On an API that declares none of the types it names explicitly, Rack::Utils.best_q_match answers the */* with the first media type in mime_types, which is the order the API declared its content types in. That order is the API's preference.

Bug

Grape::ContentTypes::MimeTypesCache is shared process-wide, so that every registry with the same content types shares one table. It was keyed on the content-types Hash, and Hash equality ignores order ({ json: 1, md: 2 } == { md: 2, json: 1 }). The first API built with a set of content types fixed the order for every later API that declared the same set in another order:

md_first = Class.new(Grape::API) do
  content_type :md, 'text/markdown'
  content_type :json, 'application/json'
  get(:page) { 'page' }
end

json_first = Class.new(Grape::API) do
  content_type :json, 'application/json'
  content_type :md, 'text/markdown'
  get(:page) { 'page' }
end

# Chrome's Accept, md_first requested first:
#   md_first   -> text/markdown
#   json_first -> text/markdown (application/json when it is the only API in the process)

mime_types_for's shortcut to MIME_TYPES compared the registry with ==, so an API declaring exactly the default content types in another order got the defaults' order.

Fix

  • mime_types_for keys MimeTypesCache on from_settings.to_a, the registry's pairs in order. Registries that declare the same content types in the same order still share one table.
  • The MIME_TYPES shortcut takes DEFAULTS itself (equal?), which is what content_types_for hands over when an API declares nothing.
  • mime_types_for runs once per middleware build and is memoized on the instance, so the Array costs nothing per request.
  • LookupCache and Formatter::FormatForAcceptCache keep their Hash keys: their tables answer one key at a time, and the answers do not depend on order. The header versioner's MediaTypeForAcceptCache is already keyed on an Array.

Tests

  • spec/grape/api_spec.rb: two APIs declaring :json/:txt in opposite orders each answer a browser Accept in their own first type.
  • spec/grape/content_types_spec.rb: each registry keeps its order, and the default content types in another order keep that order. The context that passed a copy of DEFAULTS and expected the MIME_TYPES object itself now passes DEFAULTS.
  • Mutation-checked: master's content_types.rb fails all three new examples. Restoring only == fails one, and restoring only the Hash key fails the other two.
  • bundle exec rake passes.

🤖 Generated with Claude Code

@ericproulx
ericproulx force-pushed the fix/content-type-order-cache branch from 54c879e to f79d8ab Compare October 16, 2026 19:26
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx requested a review from dblock October 16, 2026 19:32
@ericproulx
ericproulx force-pushed the fix/content-type-order-cache branch 5 times, most recently from c2d60f7 to 233c13b Compare October 17, 2026 20:42
A browser's Accept header ends in */*, and on an API whose content
types it names none of, Rack's best_q_match answers it with the first
media type the API declares: the declaration order is the API's
preference. ContentTypes::MimeTypesCache, shared process-wide so that
registries with the same content types share one table, was keyed on the
content-types Hash, and Hash equality ignores order. The first API built
with a set of content types fixed the order for every later API that
declared the same set in another order, so `content_type :json` then
`:md` could answer a browser in Markdown. mime_types_for's shortcut to
MIME_TYPES compared with == and had the same blind spot.

The cache is now keyed on the registry's pairs, in order, and the
shortcut takes DEFAULTS itself only. mime_types_for runs once per
middleware build and is memoized there, so the Array costs nothing per
request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the fix/content-type-order-cache branch from 233c13b to c4bcbc8 Compare October 19, 2026 11:41
@ericproulx
ericproulx merged commit 02332a7 into master Oct 19, 2026
102 of 103 checks passed
@ericproulx
ericproulx deleted the fix/content-type-order-cache branch October 19, 2026 11:47
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.

2 participants