Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
* [#2953](https://github.com/ruby-grape/grape/pull/2953): Answer an `error!` String message with its own status instead of a `500` when the route names a `failure` entity for that status - [@ericproulx](https://github.com/ericproulx).
* [#2955](https://github.com/ruby-grape/grape/pull/2955): Reject a value that is not an Array, and a member of none of the types, in a collection with multiple member types instead of passing them on as `nil` and `InvalidValue` - [@ericproulx](https://github.com/ericproulx).
* [#2957](https://github.com/ruby-grape/grape/pull/2957): Reject an element that is not a Hash in an Array param given a block, and stop `declared` raising on a value that is not a Hash where nested params are declared - [@ericproulx](https://github.com/ericproulx).
* [#2959](https://github.com/ruby-grape/grape/pull/2959): Send a content type for an `api_format` the API does not declare instead of a `nil` header - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

### 4.0.1 (2026-09-15)
Expand Down
6 changes: 5 additions & 1 deletion lib/grape/middleware/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,16 @@ def fetch_formatter(headers)
# this runs on every response, and copying the hash costs an allocation
# per request for a header the caller is about to send anyway.
#
# A format with no media type at all (see #media_type_for) leaves the
# header out: Rack forbids a nil header value.
#
# @param headers [Hash] the response headers, mutated in place
# @return [void]
def ensure_content_type!(headers)
return if headers[Rack::CONTENT_TYPE]

headers[Rack::CONTENT_TYPE] = content_type_for(env[Grape::Env::API_FORMAT])
content_type = media_type_for(env[Grape::Env::API_FORMAT])
headers[Rack::CONTENT_TYPE] = content_type if content_type
end

def read_body_input
Expand Down
16 changes: 14 additions & 2 deletions lib/grape/middleware/precomputed_content_types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module Grape
module Middleware
# Include in a middleware subclass that needs content-type negotiation.
# Provides +content_types+ / +mime_types+ / +content_type_for+ /
# +content_type+ resolved from +config.content_types+ and
# +media_type_for+ / +content_type+ resolved from +config.content_types+ and
# +config.format+ — so the consuming middleware's +Options+ Data class
# must declare both fields. Warms those caches on the parent instance
# at initialization so per-request +dup+s inherit them rather than
Expand All @@ -19,6 +19,9 @@ module Middleware
# Opt-in: plain +Grape::Middleware::Base+ subclasses that don't need
# content-type-aware helpers don't pay for them.
module PrecomputedContentTypes
# Grape's own media type for each built-in format, under both spellings.
DEFAULT_CONTENT_TYPES = Grape::ContentTypes.lookup_for(Grape::ContentTypes::DEFAULTS)

def initialize(app, **options)
super
content_types
Expand All @@ -37,8 +40,17 @@ def content_type_for(format)
content_types_lookup[format]
end

# The media type to label a response rendered in +format+ with: the one
# this API declares for it, or else Grape's own -- +api_format :txt+ in
# a +format :json+ API renders text/plain. #content_type_for stays nil
# for a format the API does not declare, since negotiation asks it
# whether the API supports the format at all.
def media_type_for(format)
content_type_for(format) || DEFAULT_CONTENT_TYPES[format]
end

def content_type
content_type_for(env[Grape::Env::API_FORMAT] || config.format) || 'text/html'
media_type_for(env[Grape::Env::API_FORMAT] || config.format) || 'text/html'
end

private
Expand Down
35 changes: 35 additions & 0 deletions spec/grape/api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4775,6 +4775,41 @@ def before
end
end

context ':json only' do
before { subject.format :json }

it "sends Grape's media type for an explicit api_format the API does not declare" do
subject.get '/plain' do
api_format :txt
'plain'
end
get '/plain'
expect(last_response.headers['Content-Type']).to eq('text/plain')
expect(last_response.body).to eq('plain')
end

it 'leaves the content type out for an explicit api_format with no media type' do
subject.formatter :csv, ->(object, _env) { object.to_s }
subject.get '/csv' do
api_format :csv
'a,b'
end
get '/csv'
expect(last_response.headers).not_to have_key('Content-Type')
expect(last_response.body).to eq('a,b')
end

it 'renders an error after an explicit api_format the API does not declare in its media type' do
subject.get '/plain_error' do
api_format :txt
error!('a < b', 400)
end
get '/plain_error'
expect(last_response.headers['Content-Type']).to eq('text/plain')
expect(last_response.body).to eq('a < b')
end
end

context ':serializable_hash' do
before do
stub_const(
Expand Down
Loading