-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Support HTTP server and client request streaming (single HTTP::Request) #3406
Conversation
7f9b721
to
c053950
Compare
Why not just make On This does mean reworking a lot of the API of HTTP but I think that HTTP needs a rewrite to be streaming-first anyway. Passing in strings or other static data should be secondary, and use the underlying streaming to copy the data into the exposed IO. |
@RX14 It's a good idea, and I thought about it after creating the PR. But there are two issues with that:
A solution for point 2 is to have a special IO that just wraps a String, and when performing the request we'd check that type and extract the String. |
@RX14 Hmmm... that special IO could actually just be |
@asterite If the body IO object for a We shouldn't "pass in" an IO, we should "safely expose" the underlying writable IO, and if we're passed an object to be the request body, copy that object to the IO the most efficient way. |
@RX14 I guess I'd have to see some code. I can't see how that String would go straight to the socket without going through an HTTP::Request object. I also can't imagine a nice API for directly streaming into the request. |
You would essentially have a flow like so:
Single-call APIs would take a body object that would be copied to the body IO, and return a response. Raw, linear flow: request = HTTP::Request.new("POST", "http://example.com/foo", headers: ...)
request.body << "boo bar"
response = request.finish
response.status # => 200
response.body # => IO
response.close Nicer flow: response = HTTP::Request.post("http://example.com/foo", headers: ...) do |body|
body << "foo bar"
end
response.status # => 200
response.body # => IO
response.read do |body|
# read body, close after block?
end Request body flow: response = HTTP::Request.post("http://example.com/foo", headers: ..., body: "foo bar")
response.status # => 200
... Sure you don't get the response in a block, but I think the benefit outweighs the drawbacks, and the positives of the API outweigh the negatives. Providing a Request pooling could be done through an external |
Where's the socket on those examples? Does the request hold a reference to that? |
Nevermind... I think in either case I'd need to review this with @waj and later we can propose alternatives. |
|
I'd really love to see this or #3395 for the next release 🙏 |
c053950
to
70ed1e0
Compare
161cbdd
to
e1cae1b
Compare
I've updated this PR with a few changes:
|
@@ -53,6 +53,9 @@ | |||
# of the returned IO (or used for creating a String for the body). Invalid bytes in the given encoding | |||
# are silently ignored when reading text content. | |||
class HTTP::Client | |||
# The set of possible valid body types | |||
alias BodyType = String | Bytes | IO | 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.
Maybe BodyType
shouldn't include Nil
, because it means "no body", then if you want to allow no body then thats just BodyType?
? I'm 50/50 on this change.
@@ -107,46 +107,58 @@ module HTTP | |||
|
|||
# :nodoc: | |||
def self.serialize_headers_and_body(io, headers, body, body_io, version) |
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 we need to retain the body, body_io
split? Why not just String | IO
?
This is great 👍 |
e1cae1b
to
436bbce
Compare
I think we should change |
No, because then you have to remember to close the body every time you do a request, which is tedious and bug-prone. |
@asterite I thought that was why we had the block form. Infact I think it already behaves that way. |
Just because there's a precedent of something bug-prone it doesn't mean we have to do everything in a bug-prone way. Fetching a request as a string is way too common to always have to do it with a block. Also parsing from an IO is slower than from a string, so I prefer to provide a convenient and fast way out of the box. |
@asterite I think that reading IOs into memory is more bug prone and certainly a lot less powerful than using
Most responses you get from remote services will be formatted as
In the small response case you've just waited probably at least 1ms for the HTTP response so a few extra μs likely doesn't matter. In the large response case you're likely to want an |
This is an alternative implementation to #3395
I realized there's no need to have two distinct types representing an
HTTP::Request
. I copy its docs:With that we make sure users always invoke
body_io
to stream the request body. With two request types we could remove thebody
method completely, so it'll be a compile-time error, but this exception will be pretty evident as soon as you test your endpoint, so I think that's OK.Having a single type is better in my opinion:
HTTP::Client
(with two types you'd need to correctly convert one request type to another)HTTP::Client
. For example we can pass an openFile
as a request'sbody_io
, which covers the file-upload scenario (for other cases one would probably have to use a pipe).