From bf87d685b7f8971e9c8bd49bc031a57d05e8093d Mon Sep 17 00:00:00 2001 From: Matt Hall Date: Mon, 22 Jun 2026 16:40:24 +0100 Subject: [PATCH 1/3] Add failing spec for additional envelope wrapping errors --- spec/integration/grape_entity/entity_spec.rb | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/spec/integration/grape_entity/entity_spec.rb b/spec/integration/grape_entity/entity_spec.rb index 96af0899e..4dc22ca7b 100644 --- a/spec/integration/grape_entity/entity_spec.rb +++ b/spec/integration/grape_entity/entity_spec.rb @@ -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 From 2b2ffd97fb6c43c5896f7fec47b3c5460add2cf9 Mon Sep 17 00:00:00 2001 From: Matt Hall Date: Mon, 22 Jun 2026 19:53:38 +0100 Subject: [PATCH 2/3] Fix for wrap_message incorrectly wrapping SimpleDelegator-wrapped Hash --- lib/grape/error_formatter/json.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/grape/error_formatter/json.rb b/lib/grape/error_formatter/json.rb index 9592a0b60..fe34a904b 100644 --- a/lib/grape/error_formatter/json.rb +++ b/lib/grape/error_formatter/json.rb @@ -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) From 3ec862a99eb562d6033d6e2cbd1c0c4ad3dfd69a Mon Sep 17 00:00:00 2001 From: Matt Hall Date: Mon, 22 Jun 2026 21:05:09 +0100 Subject: [PATCH 3/3] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 501c995f0..e1ebad787 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)