-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Handle ActionDispatch::Response::RackBody as body #22
base: main
Are you sure you want to change the base?
Conversation
6590400
to
d8cc3ff
Compare
Whew, I got quite scared when I read the gem is not working for you with Rails 7.1. In our project (also Rails 7.1), it does. We insert the middleware directly in the base controller (a barely-known Rails feature), can you try that out to get closer to the root cause? (You hinted middleware order may play a role.) class MyRailsController
use Idempo, backend: Idempo::ActiveRecordBackend.new, other: options
end |
I am inclined to merge this with a few changes (only relying on |
No big objections, haven't peeked into the insides of Rack here. Is there any way we can add some regression tests around this? |
@julik > only relying on to_ary if we are, in fact, under Rack 3 or greater I'll make a change. |
e77725f
to
aed365d
Compare
449bdc8
to
94d9221
Compare
94d9221
to
c8bbc89
Compare
@julik Did couple of adjustments.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Few tweaks still needed. Also maybe a good idea to note around rack_response_body.close
that this will not be applied to Arrays resulting from to_ary
@@ -58,6 +59,12 @@ def call(env) | |||
|
|||
expires_in_seconds = (headers.delete("X-Idempo-Persist-For-Seconds") || @persist_for_seconds).to_i | |||
if response_may_be_persisted?(status, headers, body) | |||
if Gem::Version.new(Rack.release) >= Gem::Version.new("3.0") | |||
# `body` could be of type `ActionDispatch::Response::RackBody` and idempo will not even attempt to store it, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's alter this comment because it is a bit misleading. A Rack response body may permit "materialization" into an Array of strings using to_ary
- this is in fact in the Rack spec, here https://github.com/rack/rack/blob/main/SPEC.rdoc#the-body- So in this case, checking for respond_to?(:to_ary)
and calling it is sufficient. For each
and call
we might need more special cases, but in this instance it is enough to state that some Rack bodies can be "expanded" using to_ary
. It doesn't matter whether such a body comes from Rails or any other callee. It is also important to note that if we did use to_ary
we are not supposed to call close
- i.e. we need to stick to the Rack semantics very carefully here, and the comments should reflect that.
if Gem::Version.new(Rack.release) >= Gem::Version.new("3.0") | ||
# `body` could be of type `ActionDispatch::Response::RackBody` and idempo will not even attempt to store it, | ||
# we're converting ActionDispatch::Response::RackBody to a storable array format. | ||
body = body.try(:to_ary) || body if !body.is_a?(Array) && !body.is_a?(Enumerator) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for any extra checkes except for the responding to to_ary
. An Array will respond to to_ary
and return self
:
[2] pry(main)> ar = []
=> []
[3] pry(main)> ar.object_id
=> 320
[4] pry(main)> ar.to_ary.object_id
=> 320
A Body being an Enumerator is another check that should not be done, as an object can perfectly be a Rack response body and respond to either each
or call
, but not be an Enumerator.
@@ -58,6 +59,12 @@ def call(env) | |||
|
|||
expires_in_seconds = (headers.delete("X-Idempo-Persist-For-Seconds") || @persist_for_seconds).to_i | |||
if response_may_be_persisted?(status, headers, body) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most likely you want to call to_ary
here, actually - because an Array of strings may be persistable, and replacing body
with the "materialized" array will allow response_may_be_persisted?
to size it correctly by a sum of bytesizes of the array elements. So first call to_ary
to materialize, then check for whether the materialized body is too big.
@@ -117,7 +124,7 @@ def response_may_be_persisted?(status, headers, body) | |||
def body_size_within_limit?(response_headers, body) | |||
return response_headers["Content-Length"].to_i <= SAVED_RESPONSE_BODY_SIZE_LIMIT if response_headers["Content-Length"] | |||
|
|||
return false unless body.is_a?(Array) # Arbitrary iterable of unknown size | |||
return false unless body.is_a?(Array) || body.respond_to?(:to_ary) # Arbitrary iterable of unknown size |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This replacement won't be needed if you materialize into an Array earlier
With our new rails 7.1 project, idempotent requests don't seem to be working. They are not even saved.
As it turns out, Idempo middleware receives an instance of
ActionDispatch::Response::RackBody
as a body. And since it's not Array, we're not saving that body.I'm not sure exactly why another project based on rails 7.0 keeps working. Because rails started returning
ActionDispatch::Response::RackBody
pre 7.0 version. I assume, that this could be due to the way how middleware is being inserted (how deep in a stack).Here is what we do in our
application.rb
:And here is an output of
rails middleware
:But that is just a wild guess.
As a fix, I suggest trying converting body to array — this proved effective in our project.