Skip to content
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

Merged
merged 2 commits into from
Jun 13, 2018

Conversation

paul
Copy link
Contributor

@paul paul commented Jun 7, 2018

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.

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.

 >> o = Object.new
=> #<Object:0x000055e0c6e69888>
>> [1, 2, 3, 4].inject(o) { |obj, i| obj }
=> #<Object:0x000055e0c6e69888>

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.

@tarcieri
Copy link
Member

tarcieri commented Jun 8, 2018

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 or just returning the original object is a no-op, and doesn't allocate any additional objects.

🎉 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.
@paul paul force-pushed the feature/generic-features branch from 431362a to 539409c Compare June 8, 2018 14:33
Copy link
Member

@janko janko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you! 👍

Copy link
Member

@tarcieri tarcieri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice refactoring

compress_all! unless @compressed
@compressed.read(*a)
end

Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

@janko janko Jun 11, 2018

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).

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Member

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.

Copy link
Contributor Author

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))
Copy link
Member

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.

Copy link
Contributor Author

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)).

Copy link
Member

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 👍

@paul
Copy link
Contributor Author

paul commented Jun 13, 2018

@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. Is that more what you had in mind? @tarcieri should probably resolve your valid bikeshed concern, too

@tarcieri
Copy link
Member

Looks good to me

@ixti ixti merged commit da68f40 into httprb:master Jun 13, 2018
@ixti
Copy link
Member

ixti commented Jun 13, 2018

Thank you!

@janko
Copy link
Member

janko commented Jun 13, 2018

@paul Thank you!

@tarcieri
Copy link
Member

Indeed, thanks @paul! And I hope this is just the start of refactoring more features this way 😄

netbsd-srcmastr pushed a commit to NetBSD/pkgsrc that referenced this pull request Jun 7, 2020
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])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants