Send a content type for an api_format the API does not declare - #2959
Open
ericproulx wants to merge 1 commit into
Open
ericproulx wants to merge 1 commit into
ericproulx wants to merge 1 commit into
Conversation
ericproulx
force-pushed
the
fix/api-format-undeclared-content-type
branch
from
September 18, 2026 14:37
1ee079d to
61ccf9d
Compare
Danger ReportNo issues found. |
`api_format` picks the format a response is rendered in, and the media type
it is labelled with comes from the formats the API declares. A format the
API does not declare has none there, so on a `format :json` API
get('/plain') { api_format :txt; 'plain' }
rendered `plain` under `content-type: nil`. Rack forbids a nil header value
and Rack::Lint raises on it; a server writes an empty header, or none. The
same goes for `:xml` and `:binary`. The only spec for `api_format :txt`
declares `content_type :txt, 'text/plain'` on its API, so the suite's
`Grape.config.lint` never met the nil.
An error raised after it went wrong the other way: the error middleware
falls back to text/html, and HTML-escapes a body it labels so, so
`error!('a < b', 400)` answered `a < b` as text/html while rendering it
with the txt error formatter.
PrecomputedContentTypes, which both middlewares share, now answers
#media_type_for: the media type the API declares for a format, or else the
one Grape registers for it by default -- text/plain, application/xml,
application/octet-stream. The formatter labels a response with it and
leaves the header out for a format with no media type anywhere, a custom
formatter's; the error middleware labels an error with it before falling
back to text/html. #content_type_for is left as it is: negotiation, format
extensions and the 415 check ask it whether the API supports a format at
all, and a fallback there would accept formats the API never declared.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
fix/api-format-undeclared-content-type
branch
from
September 18, 2026 15:39
61ccf9d to
95f7d36
Compare
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
api_formatpicks the format a response is rendered in, and the media type it's labelled with comes from the formats the API declares. A format the API doesn't declare has no entry there, so on aformat :jsonAPI:content-type: nil. Rack forbids anilheader value, andRack::Lintraises on it.text/htmland HTML-escapes any body it labels that way, even though the txt error formatter rendered it. With:xml, the whole XML document comes back escaped.Both happen on 3.3.5 too. The suite runs with
Grape.config.lint = true, but the onlyapi_format :txtspec declarescontent_type :txt, 'text/plain'on its API, so no spec reached this shape.PrecomputedContentTypes, which both middlewares share, now provides#media_type_for: the media type the API declares for a format, or else the one Grape registers for it by default (text/plain,application/xml,application/octet-stream).nil.text/htmlonly when there's still nothing.#content_type_foris unchanged. Negotiation, format extensions and the 415 check use it to decide whether the API supports a format, and a fallback there would accept formats the API never declared. All the README's content-negotiation claims still hold.format :jsonAPIapi_format :txtcontent-type: niltext/plainapi_format :xml/:binarynilapplication/xml/application/octet-streamapi_format :csv(custom formatter, no media type)nilapi_format :txtthenerror!('a < b')text/html, bodya < btext/plain, bodya < bThe fallback table is built once when the module loads. A declared format takes the same lookup as before, and only a miss reads the second table.
Test plan
:json onlycontext inapi_spec.rb: undeclared:txt, custom format with no media type, and an error afterapi_format :txt. The first two fail on master withRack::Lint::LintErrorvia the suite's lint, and the third on the escaped body. Removing the fallback from the error middleware fails the third on its own.🤖 Generated with Claude Code