From 86deee68d9594a04026adac8873edf2ad4b9599a Mon Sep 17 00:00:00 2001 From: Eric Proulx Date: Fri, 11 Sep 2026 10:31:49 +0200 Subject: [PATCH] Coerce through dry-types' non-raising call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `DryTypeCoercer#call` coerced with `@coercer[val]` and rescued `Dry::Types::CoercionError` for a value it could not coerce. That raise is what cost: dry-types' `CoercionError.handle` re-raises with `backtrace: exception.backtrace` of the error underneath, which builds that whole backtrace as Strings. At request depth a rejected value cost about 30 µs, where an accepted one costs a few hundred nanoseconds. It is paid by every `types: [Integer, String]` param given a string, since each coercer tried before the one that accepts it fails first; by every 400 for a mistyped value; and by invalid dates, numbers and booleans. dry-types' documented block form of `Type#call` reports a failure by calling the block instead of raising, so the coercer now passes one that answers `InvalidValue`. The `rescue` stays for anything that raises regardless. A rejected Integer at request depth went from 31.2 to 4.4 µs. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 1 + lib/grape/validations/types/dry_type_coercer.rb | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdc7d3a3e..a5ab02cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). +* [#2930](https://github.com/ruby-grape/grape/pull/2930): Coerce through dry-types' non-raising call so a rejected value no longer builds a backtrace - [@ericproulx](https://github.com/ericproulx). * Your contribution here. ### 4.0.0 (2026-09-07) diff --git a/lib/grape/validations/types/dry_type_coercer.rb b/lib/grape/validations/types/dry_type_coercer.rb index a39fc02db..de77418af 100644 --- a/lib/grape/validations/types/dry_type_coercer.rb +++ b/lib/grape/validations/types/dry_type_coercer.rb @@ -43,11 +43,18 @@ def initialize(type, strict: false) # Coerces the given value to a type which was specified during # initialization as a type argument. # + # Given a block, dry-types reports a value it cannot coerce by calling + # the block instead of raising. Raising is what cost: its CoercionError + # is re-raised with the backtrace of the error underneath, and building + # that backtrace as strings at request depth took about 25 µs for every + # rejected value -- every `types: [Integer, String]` param given a + # string, every 400 for a mistyped value. + # # @param val [Object] def call(val) return if val.nil? - @coercer[val] + @coercer.call(val) { InvalidValue.new } rescue Dry::Types::CoercionError InvalidValue.new end