-
Notifications
You must be signed in to change notification settings - Fork 322
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
Refactor AutoInflate/Deflate into generic features #482
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
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! 👍
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
,Request
andResponse
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.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
#inject
to 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.