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 @@ -9,6 +9,7 @@

* [#2767](https://github.com/ruby-grape/grape/pull/2767): Update rubocop to 1.88.0 and rubocop-rspec to 3.10.2 - [@ericproulx](https://github.com/ericproulx).
* [#2770](https://github.com/ruby-grape/grape/pull/2770): Avoid per-entry array allocation in `Request#build_headers` - [@ericproulx](https://github.com/ericproulx).
* [#2771](https://github.com/ruby-grape/grape/pull/2771): Fix double wrap on json errors - [@MattHall](https://github.com/MattHall).
* Your contribution here.

### 3.3.0 (2026-06-20)
Expand Down
18 changes: 10 additions & 8 deletions lib/grape/error_formatter/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ def format_structured_message(structured_message)
private

def wrap_message(message)
case message
when Hash
message
when Exceptions::ValidationErrors
message.as_json
else
{ error: ensure_utf8(message) }
end
# Use +is_a?+ rather than +case/when+ here. +case/when Hash+ matches via
# +Module#===+, a C-level real-class check that ignores delegation, so a
# +SimpleDelegator+ wrapping a Hash (e.g. the +OutputBuilder+ returned by
# +Grape::Entity#serializable_hash+ when an error is presented via an entity)
# would fall through and be wrapped in a spurious +{ error: ... }+ envelope.
# +is_a?+ is forwarded by the delegator to the wrapped Hash, so it matches.
return message if message.is_a?(Hash)
return message.as_json if message.is_a?(Exceptions::ValidationErrors)

{ error: ensure_utf8(message) }
end

def ensure_utf8(message)
Expand Down
22 changes: 22 additions & 0 deletions spec/integration/grape_entity/entity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -417,5 +417,27 @@ def static
expect(subject.body).to eql({ code: 408, static: 'some static text' }.to_json)
end
end

context 'when the format is :json' do
let(:app) do
Class.new(Grape::API) do
format :json

desc 'some desc', http_codes: [[408, 'Unauthorized', ErrorPresenter]]
get '/exception' do
error!({ code: 408 }, 408)
end
end
end

# Regression: a presented error hash must not be wrapped in an extra
# `{ "error": ... }` envelope. `Grape::Entity#serializable_hash` returns a
# `SimpleDelegator` around a Hash, which `Json#wrap_message`'s `case/when Hash`
# (via `Module#===`) fails to match, so it falls through to the `else` branch.
it 'is presented without an extra error envelope' do
expect(subject).to be_request_timeout
expect(JSON(subject.body)).to eql('code' => 408, 'static' => 'some static text')
end
end
end
end
Loading