-
Notifications
You must be signed in to change notification settings - Fork 328
Refactor AutoInflate/Deflate into generic features #482
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
Conversation
🎉 yes, thank you! |
This makes HTTP::Client, Request and Response unaware of the implementation details behind inflating and deflating stream bodies. Instead, the client iterates over all enabled features on request and response, calling `#wrap_request` or `#wrap_response`, which is expected to return the Request or Response as-is if nothing should be done, or returns a new Request or Response object after having done whatever transformation the feature requires.
431362a to
539409c
Compare
janko
left a comment
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.
LGTM, thank you! 👍
tarcieri
left a comment
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.
Nice refactoring
lib/http/features/auto_deflate.rb
Outdated
| compress_all! unless @compressed | ||
| @compressed.read(*a) | ||
| end | ||
|
|
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.
Actually, what is the reason for this method being added?
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 was trying to get the CompressedBody to quack like a Request::Body so that this branch wouldn't fail: https://github.com/httprb/http/blob/master/lib/http/request/body.rb#L20
There's probably a better approach, but I wasn't really sure how this code in Request::Body was for, so I wanted my change to remain low-impact.
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 see. But isn't CompressedBody wrapping Request::Body, not the other way around?
If I'm wrong, then in Request::Body we can modify the branch where it checks if body is Enumerable to just checking if it responds to #each instead (we don't actually need the object to be Enumerable, we're just using #each anyway).
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.
That was the first thing I tried, actually, but several other tests failed, and when I started digging in I decided that would be a larger change. Also, since Request::Body#size gets used to set the Content-Length header, I was worried that if it was passed an array-like enumerable like ["foo", "bar"], then size would be 2 instead of 6. I don't know if its ever possible for Body to ever have an array source, but it seemed easier to make AutoDeflate quack like a normal body instead.
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 arrangement in this PR makes sense to me except for Request::Body being the actual compressed body (i.e. on the wire) and CompressedBody actually being the decompressed body, though that feels like a silly bikeshed/nit
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.
Ok, I see now what's the problem. The Request::Body is being wrapped by DeflatedBody and passed as :body to Request.new, which then wraps Request::Body around it again. This causes an issue when trying to determine size, because the outer Request::Body isn't able to determine the size, even though it's CompressedBody source responds to #size. We should somehow try to avoid it, that Request#body ends up being CompressedBody.
I'm reluctant to have this PR merged as is because it loses on-the-fly compression, as CompressedBody#each is never called since Request::Body will pick the new CompressedBody#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.
@janko-m Ok, I tweaked it a bit so that CompressedBody inherits from Request::Body, and the Request doesn't try to wrap the body in a body if its already a body.
| end | ||
|
|
||
| def stream_for(connection) | ||
| Response::Body.new(Response::Inflater.new(connection)) |
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.
Do you think it would make more sense for the Inflater to wrap the Response::Body object instead of Connection? That would be symmetrical to CompressedBody wrapping the Request::Body object. And it makes more sense to me that Response::Body object reads the response body as is, and then the Inflater wrapper changes 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.
Probably. In fact, I think the entire Inflater methods could be moved into this class, similar to how AutoDeflate does it. But, again, I wanted this PR to remain low-impact.
Personally, I'm more interested in being able to add more "features" like these as plugins, like logging/instrumentation or caching. I was reading through the PR history and debating how to monkeypatch it for my own use, when I noticed that with a few minor changes to how the AutoInflate/Deflate features were implemented, then #use could become much more powerful, and enable hooks that I need without moneypatching.
I'd like for this to get merged, and then perhaps used as a jumping-off point for further refactorings and more features. There's also some ergonomic low-hanging-fruit here too (like implementing a Request#merge(opts) method, rather than each feature implementation having to reconstruct the hash by hand. For example, in this file L8-15 could be replaced with response.merge(body: stream_for(response.connection)).
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.
Yeah, that makes total sense. I do like the gradual approach to refactoring, with keeping the same functionality 👍
|
Looks good to me |
|
Thank you! |
|
@paul Thank you! |
|
Indeed, thanks @paul! And I hope this is just the start of refactoring more features this way 😄 |
Update ruby-http to 4.4.1. ## 4.4.1 (2020-03-29) * Backport [#590](httprb/http#590) Fix parser failing on some edge cases. ([@ixti]) ## 4.4.0 (2020-03-25) * Backport [#587](httprb/http#587) Fix redirections when server responds with multiple Location headers. ([@ixti]) * Backport [#599](httprb/http#599) Allow passing HTTP::FormData::{Multipart,UrlEncoded} object directly. ([@ixti]) ## 4.3.0 (2020-01-09) * Backport [#581](httprb/http#581) Add Ruby-2.7 compatibility. ([@ixti], [@janko]) ## 4.2.0 (2019-10-22) * Backport [#489](httprb/http#489) Fix HTTP parser. ([@ixti], [@fxposter]) ## 4.1.1 (2019-03-12) * Add `HTTP::Headers::ACCEPT_ENCODING` constant. ([@ixti]) ## 4.1.0 (2019-03-11) * [#533](httprb/http#533) Add URI normalizer feature that allows to swap default URI normalizer. ([@mamoonraja]) ## 4.0.5 (2019-02-15) * Backport [#532](httprb/http#532) from master. Fix pipes support in request bodies. ([@ixti]) ## 4.0.4 (2019-02-12) * Backport [#506](httprb/http#506) from master. Skip auto-deflate when there is no body. ([@Bonias]) ## 4.0.3 (2019-01-18) * Fix missing URL in response wrapped by auto inflate. ([@ixti]) * Provide `HTTP::Request#inspect` method for debugging purposes. ([@ixti]) ## 4.0.2 (2019-01-15) * [#506](httprb/http#506) Fix instrumentation feature. ([@paul]) ## 4.0.1 (2019-01-14) * [#515](httprb/http#515) Fix `#build_request` and `#request` to respect default options. ([@RickCSong]) ## 4.0.0 (2018-10-15) * [#482](httprb/http#482) [#499](httprb/http#499) Introduce new features injection API with 2 new feaures: instrumentation (compatible with ActiveSupport::Notification) and logging. ([@paul]) * [#473](httprb/http#473) Handle early responses. ([@janko-m]) * [#468](httprb/http#468) Rewind `HTTP::Request::Body#source` once `#each` is complete. ([@ixti]) * [#467](httprb/http#467) Drop Ruby 2.2 support. ([@ixti]) * [#436](httprb/http#436) Raise ConnectionError when writing to socket fails. ([@janko-m]) * [#438](httprb/http#438) Expose `HTTP::Request::Body#source`. ([@janko-m]) * [#446](httprb/http#446) Simplify setting a timeout. ([@mikegee]) * [#451](httprb/http#451) Reduce memory usage when reading response body. ([@janko-m]) * [#458](httprb/http#458) Extract HTTP::Client#build_request method. ([@tycoon]) * [#462](httprb/http#462) Fix HTTP::Request#headline to allow two leading slashes in path. ([@scarfacedeb]) * [#454](httprb/http#454) [#464](httprb/http#464) [#384](httprb/http#384) Fix #readpartial not respecting max length argument. ([@janko-m], [@marshall-lee])
This makes
HTTP::Client,RequestandResponseunaware of the implementation details behind inflating and deflating stream bodies. Instead, the client iterates over all enabled features on request and response, calling#wrap_requestor#wrap_response, which is expected to return the Request or Response as-is if nothing should be done, or returns a new Request or Response object after having done whatever transformation the feature requires.Related to #478 (and all the issues linked therein), this also provides a clean way to implement more features, like logging/instrumentation, caching, etc... In fact, several existing http.rb features could be probably be refactored into this style and clean up the codebase (proxy, json parsing, cookies, redirects...)
This pattern also avoids callbacks and chained-middleware (but it is a little similar), two things I know @tarcieri wasn't a fan of. Using
#injectto iterate over the enabled features means that the default behavior of just returning the original object is a no-op, and doesn't allocate any additional objects.I'd also like to nuke the "available features" whitelist in place of some sort of registry, or some other flexible way to add additional features to the list, but I wanted to get feedback on this approach first.