-
-
Notifications
You must be signed in to change notification settings - Fork 91
Rack 3 support #277
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
base: main
Are you sure you want to change the base?
Rack 3 support #277
Conversation
Excellent! Thanks so much for doing this. It'll be a huge win when we can make a release that supports Rack 3. Looks like there are some Rubocop errors, can you fix those? I have a few questions (for you and others):
|
thanks for checking this out @cllns ! fixed the rubocop offenses
|
Sweet! If it's possible, I think that would be best, so we can keep this a minor version release. We'll have to add rack-version to our CI matrix, but that's not a problem. It makes sense for a router. Now that I think about it, even if Rack 3 is optional, we don't want users who are setting mixed-case headers to break their apps, so we should use Rack::Headers regardless.
Outside of whether we need it, I feel like there might be reasons we want rewindable input... I guess it's more performant to keep the same input & rewind it instead of copying everything we need over. Plus users may be rewinding already (e.g. in middleware) and we don't want that to break. Curious how others feel about this. I think it might make sense to use rewindable input by default for Rack 3 (maybe based on content type?) but hopefully there's a way to automatically allow non-rewindable inputs as well, for streams. If it can't be automatic then we should try to make it simple to opt-out of rewindable inputs. I wish I knew more about Rack. I've read the UPGRADE_GUIDE, and started 'watching' the repo a couple weeks ago, so hopefully I can get a better grasp on it.
Looking into it, the Rack::ContentLength middleware only applies to responses that don't have the header already set. What if we just set it in hanami/hanami and leave it out of the router? Then we can document how users can add it. This keeps the router as small and flexible as possible. I think it's probably better to set it ourselves in the framework than rely on the middleware (which seems like more of a fallback).
This is already a big change (across several repos, thank you!), so I think it'd be wise to keep the scope of this as small as we can. Then we can work on Rack 3+ features, and ship those independently. So I think at this point:
Then we can get these merged, and work on non-rewindable inputs in separate PR's. We could ship rewindable-only Rack 3 support or wait until we actually support non-rewindable input to ship a new versions of the gems. Curious to get @timriley's feedback :) |
hi @cllns 👋 (sorry its been 10 years) i just committed some changes. some thoughts:
headersthis was the trickiest part for me, and would be very curious to hear your thoughts on it. i ran into a few different roadblocks, but think i have a solution:
these changes have achieved our own passing tests, as well as allowing the router to be able to respond correctly to multiple, technically incompatible, versions of Rack with just a few conditionals content-lengthI'm honestly slightly confused by what you mean here. i feel like automatically calculating and injecting the i'd love to keep cracking at this and want to make sure its right, so let me know any changes you'd like to see. and thank you again! |
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.
Thank you so much for doing this, @kyleplump, and thank you @cllns for the wonderful feedback on this PR so far! 😍
I agree that we should be supporting both Rack 2 and 3 for the time being. Hanami 3 (whenever that comes) would be the right time to drop Rack 2 support.
If I might attempt summarise the changes here, adding Rack 3 compatibility boils down to:
- Loosening the gem dependency (of course)
- Deciding that we'll keep rewindable input objects as a first step of this transition, since that's what we've been expecting with Rack 2 to date
- Updating our tests to expect the lower-cased headers that Rack 3 gives us
That's basically it, right?
If so, that's a really nice and simple adjustment. Easy to communicate, easy (hopefully) for others to understand. And it means the only thing we have to focus on for a full Rack 3 switch is a decision about rewindable input.
Love your work!
As for this PR, I've made a range of small suggestions via inline comments.
Biggest one is about moving the work of converting the input to rewindable up into Router#call
, since I think that is an important behaviour, and keeping it buried in Router#_params
makes it harder for us and our future contributors to find and understand.
Have a read of it all and please let know if there's anything else you'd like to discuss!
@@ -39,10 +39,10 @@ def initialize(app, parsers) | |||
end | |||
|
|||
def call(env) | |||
body = env[RACK_INPUT].read | |||
return @app.call(env) if body.empty? | |||
body = env[RACK_INPUT]&.read |
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.
What required this &.
change?
I'm sure you put it there for good reason, I just want to understand why. 😄 Especially because there's nothing in the rack 3 upgrade guide about env["rack.input"]
potentially being nil
.
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.
its actually to fix a failing spec
i figure if its failing in our existing test suite, that its chance of occurring for others is non-negligible and would be an extra safe guard. happy to remove and rework the spec if you'd rather take that path!
@@ -30,6 +30,7 @@ def self.mime_types | |||
# @since 2.0.1 | |||
# @api private | |||
def parse(*, env) | |||
env[::Rack::RACK_INPUT].rewind if env[::Rack::RACK_INPUT].respond_to?(:rewind) |
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.
Given the Rack 2 input was rewindable, I wonder why we weren't rewinding previously to this? Was it simply that no one actually read the stream before this middleware was hit, in reality?
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.
I'm not sure - although what you're saying could very well be the case. Rack 3's more stringent requirements around this could have unearthed a future issue!
lib/hanami/router.rb
Outdated
|
||
if !env.key?(ROUTER_PARSED_BODY) && (input = env[::Rack::RACK_INPUT]) and input.rewind | ||
if !env.key?(ROUTER_PARSED_BODY) && input |
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.
There's a behavioural change here: we're no longer rewinding the input—see the and input.rewind
in previous version of this line—before we parse the input in the line below.
Is there any reason we shouldn't be keeping that here to preserve our existing behaviour?
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.
nope - this was a mistake and why PR's exist!
lib/hanami/router/block.rb
Outdated
# explicitly set 'content-length' header | ||
headers[::Rack::CONTENT_LENGTH] = body.size.to_s |
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.
The comment here is a but redundant. We can see from the line of code below that we're explicitly setting the header. What I'd like to see the comment explain is why we're doing it here. We want to make the why clear to the reader.
Speaking of which, why are we doing this here? 😅
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.
After re-reading the conversation with @cllns so far, I think we've added this in because of this suggestion from Sean?
Similarly, for Content-Length, do we want add Rack::ContentLength middleware to all hanami-router apps, by default?
If we're wanting to keep this PR (and everything else to do with the Rack 3 compatibility changes) as minimal as possible, then I think it might be better to consider and make this change (or not) as a separate matter?
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.
Sorry, I missed the detail about Content-Length from rack/rack#1472 (comment). Now I'm now caught up.
I think we should leave Content-Length
out. People on the latest version of Rack 2 are already missing this header anyway.
If we wanted to restore it, I think we could consider adding the Rack::ContentLength
middleware as part of the default middleware stack in Hanami apps.
People using the router directly can choose to do the same, too. I think it's better to keep the router as lean as practical.
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.
My understanding is its actually the opposite: Rack 2 were the only people getting the header on the rack responses. The inclusion here was for green specs, but am super willing to either remove the explicit header checks from the specs, or go the middleware route
lib/hanami/router.rb
Outdated
@@ -916,15 +922,18 @@ def _redirect(to, code) | |||
def _params(env, params) | |||
params ||= {} | |||
env[PARAMS] ||= {} | |||
input = Rack::RewindableInput.new(env[::Rack::RACK_INPUT]) if env[::Rack::RACK_INPUT] |
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.
I wonder if we're "burying the lede" a little bit by doing this conversion of the input to a rewindable input down here in this _params
.
The job of the _params
method is to extract params, and by adding this line, I feel like we're asking it to do more than that, which isn't a good design change.
If we're deciding that we want to force a rack 3 input to be rewindable as the first part of our rack 3 transition, couldn't we do this conversion right up in Router#call
?
Router#call
is is the main entry point for the router, and as such, it feels like a more appropriate place to do something like this. It would make the rewindable conversion more prominent, and give us a clearer place to comment why we're doing it (this is important, IMO).
Can you please have a go at moving it there?
And as part of moving it there, I wonder if we actually want to wrap it in the rack_3? conditional, e.g.
# Explanatory comment goes here
if rack_3? && (input = env[::Rack::RACK_INPUT])
env[::Rack::RACK_INPUT] = Rack::RewindableInput.new(input)
end
Having this lead with the rack_3?
conditional would also make it clearer that it's part of our Rack 3 transition strategy.
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.
yes, agree with all these points! will make the update
Co-authored-by: Tim Riley <[email protected]>
Co-authored-by: Tim Riley <[email protected]>
Co-authored-by: Tim Riley <[email protected]>
Co-authored-by: Tim Riley <[email protected]>
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.
thanks for the review @timriley! i at least responded to all your comments / committed the suggestions. I have a few slightly larger updates on my end and will ping you back!
@@ -30,6 +30,7 @@ def self.mime_types | |||
# @since 2.0.1 | |||
# @api private | |||
def parse(*, env) | |||
env[::Rack::RACK_INPUT].rewind if env[::Rack::RACK_INPUT].respond_to?(:rewind) |
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.
I'm not sure - although what you're saying could very well be the case. Rack 3's more stringent requirements around this could have unearthed a future issue!
lib/hanami/router.rb
Outdated
@@ -916,15 +922,18 @@ def _redirect(to, code) | |||
def _params(env, params) | |||
params ||= {} | |||
env[PARAMS] ||= {} | |||
input = Rack::RewindableInput.new(env[::Rack::RACK_INPUT]) if env[::Rack::RACK_INPUT] |
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.
yes, agree with all these points! will make the update
lib/hanami/router.rb
Outdated
|
||
if !env.key?(ROUTER_PARSED_BODY) && (input = env[::Rack::RACK_INPUT]) and input.rewind | ||
if !env.key?(ROUTER_PARSED_BODY) && input |
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.
nope - this was a mistake and why PR's exist!
lib/hanami/router/block.rb
Outdated
# explicitly set 'content-length' header | ||
headers[::Rack::CONTENT_LENGTH] = body.size.to_s |
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.
My understanding is its actually the opposite: Rack 2 were the only people getting the header on the rack responses. The inclusion here was for green specs, but am super willing to either remove the explicit header checks from the specs, or go the middleware route
@timriley pinging you back. made a bunch of requested updates, and left a few comments 🙂 please let me know what you think. thanks! |
multiple gems are impacted and updated by this change. this gem is the most significantly changed. i'll link to the open PR's of the affected gems below. please let me know if you'd like to see any changes, thanks!
highlights
changelog for Rack 3: https://github.com/rack/rack/blob/main/UPGRADE-GUIDE.md
response headers are now required to be lower case
rack.input
is now no longer rewindablesome specs needed updating, on account that the
content-length
response header no longer a part of the implementation ofRack::Response
. this change happened in v2.1, and while not directly related to Rack 3, was included here to get all greenimpacted gems