Skip three redundant steps on every error response - #2929
Closed
ericproulx wants to merge 1 commit into
Closed
ericproulx wants to merge 1 commit into
ericproulx wants to merge 1 commit into
Conversation
- `Middleware::Error#rack_response` built a `Grape::Util::Header` from the headers only to hand it to `Rack::Response.new`, which copies its argument into a Headers of its own on every supported Rack. Rack 2.2's `HeaderHash[]` adopts only a Hash that already is one, and 3.x always builds a fresh `Rack::Headers`. #2911 kept this copy because callers write into what they are given, but here the writer is Rack::Response, which writes into its own copy, and all three callers build the Hash fresh for this response. The copy #2911 kept in `API::Instance#call`, which can be handed a mounted app's frozen Hash, stays. - `run_rescue_handler` ran every handler through `instance_exec`, turning a Method into a Proc first. The middleware's own handlers and the endpoint's for a `with:` Symbol are Methods, already bound to a receiver that `instance_exec` cannot change, so they are now called as they are. Blocks still run as the endpoint. - `ErrorFormatter::Json#ensure_utf8` re-encoded every String message, which for one that already is valid UTF-8 only copies it, for about 260 ns. Such a message is now returned as it is; anything else is converted as before. What `ensure_utf8` does with a message that is not valid UTF-8 had no spec; dropping the encoding half of the new check passed the suite. The new spec pins all three cases: UTF-8, binary and malformed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ericproulx
force-pushed
the
perf/error-response-copies
branch
from
September 10, 2026 20:11
37f57c8 to
d747e80
Compare
Danger ReportNo issues found. |
This was referenced Sep 11, 2026
Contributor
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three steps every error response took for nothing:
Middleware::Error#rack_responsecopied the headers into aGrape::Util::Headeronly to hand them toRack::Response.new, which copies its argument into a Headers of its own on every supported Rack: Rack 2.2'sHeaderHash[]adopts only a Hash that already is aHeaderHash, and 3.x always builds a freshRack::Headers. Copy response headers with merge! instead of merge #2911 kept this copy on the grounds that callers write into what they are given, but here the writer isRack::Response, which writes into its own copy, and all three callers build the Hash fresh for this response. The copy Copy response headers with merge! instead of merge #2911 kept inAPI::Instance#call, which can be handed a mounted app's frozen Hash, stays.run_rescue_handlerran every handler throughinstance_exec, turning aMethodinto a Proc first. The middleware's own handlers (method(:error_response),method(:default_rescue_handler)) and the endpoint's for awith:Symbol are Methods, already bound to a receiver thatinstance_execcannot change, so they are now called as they are. Blocks still run as the endpoint.ErrorFormatter::Json#ensure_utf8re-encoded every String message. For one that already is valid UTF-8,encodeonly returns a copy, for about 260 ns. Such a message is now returned as it is; anything else is converted as before.Benchmarks
Median of 7 interleaved subprocess rounds against master, Ruby 4.0.6, no JIT:
MethodNotAllowed)error!rescue_fromblock callingerror!rescue_from :allBehaviour
Byte-identical to master (status, headers and body) over a 228-case matrix: JSON, txt, XML and content-negotiated APIs ×
error!with a String, a Hash, custom headers, an HTML content type, and frozen, binary, malformed and US-ASCII messages;rescue_fromwith a block, awith:Symbol, an arity-0 block, a handler that returns junk, one that raises again and one that returns aRack::Response; unhandled exceptions, validation errors, 405, OPTIONS and 404; each with three Accept headers.Missing spec, added
What
ensure_utf8does with a message that is not valid UTF-8 had no spec: dropping the encoding half of the new check passed the whole suite while a binary message went out as raw bytes (json 3 warns this "will raise an encoding error").spec/grape/error_formatter/json_spec.rbpins the three cases: UTF-8 as it is, binary converted with the unmappable bytes replaced, and malformed UTF-8 with the invalid bytes replaced. It passes before and after this change.Test plan
with:handlers); skipping the validity half of the UTF-8 check fails 2; skipping the encoding half fails 1 (the new spec).🤖 Generated with Claude Code