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

Better support for connection upgrade and bi-directional streaming. #101

Merged
merged 4 commits into from Jan 27, 2023

Conversation

ioquatix
Copy link
Member

@ioquatix ioquatix commented Jan 26, 2023

Previously, there was no way to implement connection upgrade or proper streaming. The systems I've checked which attempt to do this end up monkey patching webrick. So, let's make it possible without monkey patching. This is required for Rack 3.

This allows streaming which uses connection: close when no content-length is known. It also avoided buffering the entire response which causes problems for streaming real-time responses.

Copy link
Contributor

@jeremyevans jeremyevans left a 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 This is required for Rack 3 is accurate. I cannot find anything in the Rack 3 SPEC that requires support for the upgrade header or that bodies cannot be buffered. The Rack 3 spec does state that middleware cannot call each, but it says nothing about what servers can do with bodies.

Switching from buffered to unbuffered mode for bodies can be a denial-of-service risk to applications, so I think that should be opt-in. Please remember that the vast majority of webrick use is request/response and not streaming. This is obvious currently because streaming doesn't work at all, but even if streaming did work, the vast majority of webrick use would still be request/response. We can enable streaming support on an opt-in basis without exposing existing webrick users to denial of service vulnerabilities.

lib/webrick/httpresponse.rb Outdated Show resolved Hide resolved
lib/webrick/httpresponse.rb Show resolved Hide resolved
lib/webrick/httpresponse.rb Outdated Show resolved Hide resolved
lib/webrick/httpresponse.rb Show resolved Hide resolved
Copy link
Contributor

@jeremyevans jeremyevans left a comment

Choose a reason for hiding this comment

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

One minor one-line change requested, then this is good to merge without further review.

if @bodytempfile
@bodytempfile.rewind
IO.copy_stream(@bodytempfile, socket)
else
@body.call(socket)
end
@sent_size = size

if content_length = @header['content-length']
Copy link
Contributor

Choose a reason for hiding this comment

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

Turns out that since @sent_size is set to 0 in initialize, this doesn't currently have an effect. Still, this conditional seems like a better approach.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes I'm fine with that behaviour, or alternatively we could set it to some other value, e.g. nil to represent "unknown".

elsif @body.respond_to? :call
make_body_tempfile
else
if @body.respond_to?(:bytesize)
@header['content-length'] = (@body ? @body.bytesize : 0).to_s
Copy link
Contributor

Choose a reason for hiding this comment

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

(@body ? @body.bytesize : 0).to_s looks weird if you already know that @body responds to bytesize. With the change in this PR, probably this should be @body.bytesize.to_s

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah yeah, good call, I just copied the previous code.

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, fixed.

@ioquatix
Copy link
Member Author

ioquatix commented Jan 27, 2023

I'm not sure This is required for Rack 3 is accurate. I cannot find anything in the Rack 3 SPEC that requires support for the upgrade header or that bodies cannot be buffered.

The streaming body is expected to be streaming, at least to the extent required to support WebSockets, and it should behave like the previously defined full hijack which states (IIRC something to the effect of) that reading and writing should be full-duplex.

@jeremyevans
Copy link
Contributor

I'm not sure This is required for Rack 3 is accurate. I cannot find anything in the Rack 3 SPEC that requires support for the upgrade header or that bodies cannot be buffered.

The streaming body is expected to be streaming, at least to the extent required to support WebSockets, and it should behave like the previously defined full hijack which states (IIRC) that reading and writing should be full-duplex.

The Full Hijack section does require that a single IO instance is used that can be used to read and write to the underlying connection, which basically requires a Socket, so arguably that does not allow for buffering by the server itself.

The Partial Hijack section (which Streaming Body is equivalent to) does say intended to be used when applications need bi-directional streaming, so again, arguably that implies the server shouldn't buffer, even if there isn't explicit language forbidding it.

You are correct that a buffered approach would not work for WebSockets, so in practice, any server that wants to implement WebSockets needs to use an unbuffered approach for them. So even if buffering is not explicitly forbidden by SPEC, it wouldn't be appropriate for WebSocket use.

@ioquatix
Copy link
Member Author

I think the action item here is, if we can provide additional clarifications in the spec, if you think of anything we can add to make this clearer, please open a PR :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants