Skip to content

Send a content type for an api_format the API does not declare - #2959

Open
ericproulx wants to merge 1 commit into
masterfrom
fix/api-format-undeclared-content-type
Open

ericproulx wants to merge 1 commit into
masterfrom
fix/api-format-undeclared-content-type

Conversation

@ericproulx

@ericproulx ericproulx commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Summary

api_format picks 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 a format :json API:

get('/plain') { api_format :txt; 'plain' }
# => 200, body "plain", content-type: nil

get('/oops') { api_format :txt; error!('a < b', 400) }
# => 400, content-type: text/html, body "a &lt; b"
  • Success path: the formatter sends content-type: nil. Rack forbids a nil header value, and Rack::Lint raises on it.
  • Error path: the error middleware falls back to text/html and 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 only api_format :txt spec declares content_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).

  • The formatter labels the response with it. For a format with no media type anywhere, such as a custom formatter's, it leaves the header out instead of sending nil.
  • The error middleware labels the error with it, and falls back to text/html only when there's still nothing.

#content_type_for is 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 :json API before after
api_format :txt content-type: nil text/plain
api_format :xml / :binary nil application/xml / application/octet-stream
api_format :csv (custom formatter, no media type) nil header absent
api_format :txt then error!('a < b') text/html, body a &lt; b text/plain, body a < b
a declared format unchanged unchanged

The 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

  • New :json only context in api_spec.rb: undeclared :txt, custom format with no media type, and an error after api_format :txt. The first two fail on master with Rack::Lint::LintError via the suite's lint, and the third on the escaped body. Removing the fallback from the error middleware fails the third on its own.
  • Full RSpec suite passes locally (2919 examples).
  • RuboCop clean.
  • CI green.

🤖 Generated with Claude Code

@ericproulx
ericproulx force-pushed the fix/api-format-undeclared-content-type branch from 1ee079d to 61ccf9d Compare September 18, 2026 14:37
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

`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 &lt; 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
ericproulx force-pushed the fix/api-format-undeclared-content-type branch from 61ccf9d to 95f7d36 Compare September 18, 2026 15:39
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