Skip to content
Closed
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 @@ -13,6 +13,7 @@
* [#2918](https://github.com/ruby-grape/grape/pull/2918): Skip the dry-types round trip when a value already is the declared type - [@ericproulx](https://github.com/ericproulx).
* [#2917](https://github.com/ruby-grape/grape/pull/2917): Read path captures out of the router's union match instead of re-running the route's pattern - [@ericproulx](https://github.com/ericproulx).
* [#2921](https://github.com/ruby-grape/grape/pull/2921): Pin the router's request-time isolation regressions through requests instead of its instance variables - [@ericproulx](https://github.com/ericproulx).
* [#2932](https://github.com/ruby-grape/grape/pull/2932): Let a parser's Grape errors through the formatter instead of rescuing them to raise again - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

### 4.0.0 (2026-09-07)
Expand Down
16 changes: 13 additions & 3 deletions lib/grape/middleware/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,23 @@ def read_rack_input(body)
end
env[Rack::RACK_REQUEST_FORM_INPUT] = env[Rack::RACK_INPUT]
end
rescue Grape::Exceptions::Base => e
raise e
rescue StandardError => e
rescue ForeignParserError => e
throw :error, Grape::Exceptions::ErrorResponse.new(status: 400, message: e.message, backtrace: e.backtrace, original_exception: e)
end
end

# What a parser raises that is not a Grape error, and so is answered as a
# 400 here. A Grape error goes on to the error middleware as it is. It
# used to be rescued just to be raised again, and re-raising at request
# depth cost about 25 µs -- paid by every malformed body, since the
# built-in parsers report one as InvalidMessageBody.
module ForeignParserError
def self.===(exception)
exception.is_a?(StandardError) && !exception.is_a?(Grape::Exceptions::Base)
end
end
private_constant :ForeignParserError

# this middleware will not try to format the following content-types since Rack already handles them
# when calling Rack's `params` function
# - application/x-www-form-urlencoded
Expand Down
23 changes: 23 additions & 0 deletions spec/grape/middleware/formatter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -527,4 +527,27 @@ def self.call(_, _)
expect(error.original_exception.class).to eq StandardError
end
end

# Only a parser's StandardErrors are answered with a 400. Anything else --
# an Interrupt, a SystemExit -- is not the body's fault and keeps going.
context 'custom parser raises an exception that is not a StandardError' do
it 'lets it through rather than answering 400' do
subject = described_class.new(
app,
parsers: { json: ->(_object, _env) { raise NotImplementedError, 'fatal' } }
)
io = StringIO.new('{}')
expect do
catch(:error) do
subject.call(
Rack::PATH_INFO => '/info',
Rack::REQUEST_METHOD => Rack::POST,
'CONTENT_TYPE' => 'application/json',
Rack::RACK_INPUT => io,
'CONTENT_LENGTH' => io.length.to_s
)
end
end.to raise_error(NotImplementedError, 'fatal')
end
end
end
Loading