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

Feature: chunked encoding #58

Merged
merged 46 commits into from
Sep 14, 2016
Merged

Feature: chunked encoding #58

merged 46 commits into from
Sep 14, 2016

Conversation

WyriHaximus
Copy link
Member

@WyriHaximus WyriHaximus commented Jul 4, 2016

Encountered this while writing the twitter stream example for https://blog.wyrihaximus.net/2015/11/reactphp-http-client/. Did some research and wrote up chunked transfer coding support.

Todo:

  • Decoder
  • Decoder Tests
  • Reponse
  • Reponse Test
  • Update readme
  • Chunked extensions
  • Lowercase headers

@WyriHaximus WyriHaximus self-assigned this Jul 4, 2016
@WyriHaximus WyriHaximus added this to the v0.4.11 milestone Jul 4, 2016
$stream->on('data', array($this, 'handleData'));
$stream->on('error', array($this, 'handleError'));
$stream->on('end', array($this, 'handleEnd'));
if (isset($this->headers['transfer-encoding']) && $this->headers['transfer-encoding'] == 'chunked') {
Copy link
Member

Choose a reason for hiding this comment

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

Minor nitpicking: I'd like to see strict === comparision here.

Copy link
Member

Choose a reason for hiding this comment

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

The headers should be normalized to lowercase keys and values before checking.

$normalizedHeaders = array_change_key_case($headers, CASE_LOWER);

if (isset($normalizedHeaders['transfer-encoding']) && strtolower($normalizedHeaders['transfer-encoding']) === 'chunked') {
}

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 that is something I've thought about. Currently thinking about making lowercase the default for the entire package and releasing that in 0.5.

Copy link
Member

Choose a reason for hiding this comment

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

That makes sense for response headers, request headers should be used as passed in by the user (imho).

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 absolutely, was only talking about the response headers, sorry for the confusion.

@WyriHaximus
Copy link
Member Author

@jsor I've implemented the chunked extensions and emitting them with the data event, (bfba037). The overhead of that is neglectable. In the last commit (4ca65b3) those extra's are also emitted with the data even on the response, but doing that feels a bit weird. What is your take?

@jsor
Copy link
Member

jsor commented Aug 2, 2016

@WyriHaximus Sorry, i somehow missed your comment :/

👍 on emitting the chunked extension from the decoder.
I'm undecided on emitting on the response. Mostly because it's not always clear what this extra is. Maybe an additional event or an array ['chunkedExtension' => '...'] would be more explicit

@WyriHaximus
Copy link
Member Author

@jsor No probem, it happens :).

Just updated the PR with an array as extra instead of a string.

ping @clue 👍 / 👎 so we can merge and tag this

@@ -81,7 +81,9 @@ protected function iterateBuffer()
$this->emit('data', array(
substr($this->buffer, 0, $chunkLength),
$this,
$this->chunkedExtension
[
'chunkedExtension' => $this->chunkedExtension,
Copy link
Member

Choose a reason for hiding this comment

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

It should probably be chunkExtension not chunkedExtensionas i propesed in my comment.

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right 👍

@WyriHaximus
Copy link
Member Author

FYI I've also change the ChunkedStreamDecoder chunkedExtension property to chunkExtension matching the emitted array (and because it should have been that anyway).

@jsor
Copy link
Member

jsor commented Aug 3, 2016

@WyriHaximus: Great 👍

LGTM now :shipit:

use React\Stream\DuplexStreamInterface;
use React\Stream\Util;

class ChunkedStreamDecoder
Copy link
Member

Choose a reason for hiding this comment

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

This should probably implement ReadableStreamInterface? (e.g. https://github.com/clue/php-utf8-react/blob/master/src/Sequencer.php#L13)

return;
}

if ($this->buffer === '') {
Copy link
Member Author

Choose a reason for hiding this comment

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

Currently PHP5 tests are failing over this. Changing === to == fixes it. Investigating why.

Copy link
Member Author

Choose a reason for hiding this comment

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

Found the issue, in case substr fails it will return false. Forcing the buffer back into a sting in case that happens.

@WyriHaximus
Copy link
Member Author

@clue Just looked into the twitter stream and it just sends an empty line over the connection to keep it alive:
Twitter keeping the connection alive

$this->stream->removeListener('data', array($this, 'handleData'));
$this->stream->removeListener('end', array($this, 'handleEnd'));
$this->emit('end');
return false;
Copy link
Member Author

Choose a reason for hiding this comment

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

@clue it made most sense to me to remove the listeners and just emit end. This lets the stream intact and to be reused, although that functionality would come with a later PR implementing keep alive and/or HTTP/2 multiplexing

Copy link
Member

Choose a reason for hiding this comment

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

This should probably still involve a proper close event as this stream is no longer valid readable after end has been emitted.

Also, this check does not account for chunked encoding trailers (which we should discard?).

Copy link
Member Author

Choose a reason for hiding this comment

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

Righto will add that: https://tools.ietf.org/html/rfc2616#section-14.40 So just $this->close(); because that will also close the underlying stream?

Copy link
Member

Choose a reason for hiding this comment

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

Something like this should do it:

$this->emit('end');
$this->close()`;

Yes, this will close the underlying stream, but IMO this is not something we should worry about right now. Connection reuse will require more effort anyway.

(Also, RFC 2616 is deprecated, you should consult https://tools.ietf.org/html/rfc7230#section-4.1.2 and in particular the following section instead)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah that is what I have locally now 👍 . Yeah we'll look into connection reuse in a later PR.

Should we just ignore the trailers, as we're also doing that with the chunk headers?

Copy link
Member

Choose a reason for hiding this comment

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

👍

Copy link

@joelwurtz joelwurtz Aug 23, 2016

Choose a reason for hiding this comment

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

Normally trailers are not send by the server unless you specify "trailers" in the TE header of the request so this should be safe to just never think about them (unless i'm wrong about the spec ?)

Copy link
Member Author

Choose a reason for hiding this comment

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

The thing is servers are written by developers, developers tend to have a mind of their own, developers sometimes tend to ignore specs for their own reasons or advantage. Tbh I rather not take that risk of a crashing client over something take max an hour to implements 😄 . (When working on this package it wouldn't be the first time a server just blindly sends us a gzip encoded response when we didn't specify support for it at that time.)

@WyriHaximus
Copy link
Member Author

Ping @clue

use React\Stream\Util;
use React\Stream\WritableStreamInterface;

class ChunkedStreamDecoder implements ReadableStreamInterface
Copy link
Member

Choose a reason for hiding this comment

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

Should we mark this as @internal or should consumers of this library be allowed to rely on this?

Afaict this should only be used internally and should not be part of our public API.

@clue
Copy link
Member

clue commented Sep 13, 2016

Minor remarks only. This should be squashed to a reasonable number of commits before merging, otherwise LGTM 👍

@WyriHaximus
Copy link
Member Author

Cheers, I'll squash them into one when I get home 👍

@WyriHaximus WyriHaximus merged commit 7662852 into master Sep 14, 2016
@cboden
Copy link
Member

cboden commented Sep 14, 2016

👍 😀

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

Successfully merging this pull request may close these issues.

5 participants