-
-
Notifications
You must be signed in to change notification settings - Fork 61
Feature: chunked encoding #58
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
Changes from 7 commits
b4719e1
91cb3a5
fb14e64
d6d2c1e
dd04416
897c4a1
b51cdb4
e1a3708
bfba037
a3b9c80
4ca65b3
0eab14b
f9067ce
85b0c93
03d271a
18f044a
cd4ba40
537de43
42201c8
48763cf
b32a188
6e1ebcc
69ab9c6
d4eda4d
804902f
29e283b
0b43386
07f649a
3ca85d6
7f303d8
01ecdcb
37dfdf9
61fb8a3
e4bf1ea
69e3335
6aa86db
eab3c71
34ee759
4f95569
09cde22
d7d0473
95277ff
9d4896f
8540b4e
415d790
49e9e9e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| <?php | ||
|
|
||
| namespace React\HttpClient; | ||
|
|
||
| use Evenement\EventEmitterTrait; | ||
| use React\Stream\DuplexStreamInterface; | ||
| use React\Stream\Util; | ||
|
|
||
| class ChunkedStreamDecoder | ||
|
||
| { | ||
| const CRLF = "\r\n"; | ||
|
|
||
| use EventEmitterTrait; | ||
|
|
||
| /** | ||
| * @var string | ||
| */ | ||
| protected $buffer = ''; | ||
|
|
||
| /** | ||
| * @var int | ||
| */ | ||
| protected $remainingLength = 0; | ||
|
|
||
| /** | ||
| * @var bool | ||
| */ | ||
| protected $nextChunkIsLength = true; | ||
|
|
||
| /** | ||
| * @var DuplexStreamInterface | ||
| */ | ||
| protected $stream; | ||
|
|
||
| /** | ||
| * @param DuplexStreamInterface $stream | ||
| */ | ||
| public function __construct(DuplexStreamInterface $stream) | ||
|
||
| { | ||
| $this->stream = $stream; | ||
| $this->stream->on('data', array($this, 'handleData')); | ||
| Util::forwardEvents($this->stream, $this, [ | ||
| 'error', | ||
| 'end', | ||
|
||
| ]); | ||
| } | ||
|
|
||
| public function handleData($data) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| { | ||
| $this->buffer .= $data; | ||
|
|
||
| do { | ||
| $this->iterateBuffer(); | ||
| } while (strlen($this->buffer) > 0 && strpos($this->buffer, static::CRLF) !== false); | ||
|
||
| } | ||
|
|
||
| protected function iterateBuffer() | ||
| { | ||
| if ($this->nextChunkIsLength) { | ||
| $this->nextChunkIsLength = false; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This block does not currently check the chunk header is actually complete. Should add a test for an incomplete chunk header without a trailing CRLF? |
||
| $this->remainingLength = hexdec(substr($this->buffer, 0, strpos($this->buffer, static::CRLF))); | ||
|
||
| $this->buffer = substr($this->buffer, strpos($this->buffer, static::CRLF) + 2); | ||
| } | ||
|
|
||
| if ($this->remainingLength > 0) { | ||
| $chunkLength = $this->getChunkLength(); | ||
| $this->emit('data', array( | ||
| substr($this->buffer, 0, $chunkLength), | ||
| $this | ||
| )); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sold on the idea we should emit the chunk extension as part of the data message. Personally, I would rather remove this for now (reduces complexity). We can still think about introducing this once we see an actual need and we can figure out a decent API without introducing a BC break. Also, in my projects I only emit the actual data as the other event attributes have always been unreliable across the React ecosystem and it's very easy to use a closure to add arbitrary references to the event handler anyway (e.g. https://github.com/clue/php-utf8-react/blob/master/src/Sequencer.php#L111) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Very valid point, especially since there is no use case. I'll remove it |
||
| $this->remainingLength -= $chunkLength; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a design decision here: Do we really want to emit incomplete chunks or do we honor the chunk header and then only emit complete chunks? I'm currently undecided on this, but would likely have suggested to honor the header. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reasoning behind not honoring the header is to keep as less data floating around somewhere in a buffer and handing it over to who ever is using the client as soon as possible. I don't have any strong preference regarding this. Can write up a benchmark to see which method performs best in various situations, but not sure if that is worth the effort 😄 . There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point: If the header says the next chunk is 2 GB in size, we should probably not allocate a 2 GB buffer :) Let's keep this as-is, but perhaps consider adding some documentation and/or tests? 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will add docs and tests 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests have been added by the way |
||
| $this->buffer = substr($this->buffer, $chunkLength); | ||
| return; | ||
| } | ||
|
|
||
| $this->nextChunkIsLength = true; | ||
| $this->buffer = substr($this->buffer, 2); | ||
| } | ||
|
|
||
| protected function getChunkLength() | ||
| { | ||
| $bufferLength = strlen($this->buffer); | ||
|
|
||
| if ($bufferLength >= $this->remainingLength) { | ||
| return $this->remainingLength; | ||
| } | ||
|
|
||
| return $bufferLength; | ||
| } | ||
|
|
||
| public function end($data = null) | ||
| { | ||
| $this->stream->end($data); | ||
|
||
| } | ||
|
|
||
| public function pause() | ||
| { | ||
| $this->stream->pause(); | ||
| } | ||
|
|
||
| public function resume() | ||
| { | ||
| $this->stream->resume(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,9 +34,13 @@ public function __construct(DuplexStreamInterface $stream, $protocol, $version, | |
| $this->reasonPhrase = $reasonPhrase; | ||
| $this->headers = $headers; | ||
|
|
||
| $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') { | ||
|
||
| $this->stream = new ChunkedStreamDecoder($stream); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Technically, this introduces a BC break. How can a consumer of this library tell if the Should we raise to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe remove this header on the request if it has been processed ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds like a solid way to go, what do you think @clue? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is also what I've had in mind, let's give it a try 👍 Documentation and/or tests would be much appreciated 👍 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do 👍 |
||
|
|
||
| $this->stream->on('data', array($this, 'handleData')); | ||
| $this->stream->on('error', array($this, 'handleError')); | ||
| $this->stream->on('end', array($this, 'handleEnd')); | ||
| } | ||
|
|
||
| public function getProtocol() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| <?php | ||
|
|
||
| namespace React\Tests\HttpClient; | ||
|
|
||
| use React\HttpClient\ChunkedStreamDecoder; | ||
| use React\Stream\ThroughStream; | ||
|
|
||
| class DecodeChunkedStreamTest extends TestCase | ||
| { | ||
| public function provideChunkedEncoding() | ||
| { | ||
| return [ | ||
| [["4\r\nWiki\r\n5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"]], | ||
| [["4\r\nWiki\r\n", "5\r\npedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"]], | ||
| [["4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n\r\nchunks.\r\n0\r\n\r\n"]], | ||
| [["4\r\nWiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"]], | ||
| [["4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n in\r\n", "\r\nchunks.\r\n0\r\n\r\n"]], | ||
| [["4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n", " in\r\n", "\r\nchunks.\r\n0\r\n\r\n"]], | ||
| [["4\r\n", "Wiki\r\n", "5\r\n", "pedia\r\ne\r\n", " in\r\n", "\r\nchunks.\r\n", "0\r\n\r\n"]], | ||
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * @test | ||
| * @dataProvider provideChunkedEncoding | ||
| */ | ||
| public function testChunkedEncoding(array $strings) | ||
| { | ||
| $stream = new ThroughStream(); | ||
| $response = new ChunkedStreamDecoder($stream); | ||
| $buffer = ''; | ||
| $response->on('data', function ($data) use (&$buffer) { | ||
| $buffer .= $data; | ||
| }); | ||
| foreach ($strings as $string) { | ||
| $stream->write($string); | ||
| } | ||
| $this->assertSame("Wikipedia in\r\n\r\nchunks.", $buffer); | ||
| } | ||
| } |
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.
This should probably implement
ReadableStreamInterface? (e.g. https://github.com/clue/php-utf8-react/blob/master/src/Sequencer.php#L13)