Skip to content
This repository was archived by the owner on Jan 30, 2020. It is now read-only.

Commit 9422140

Browse files
committed
Merge branch 'hotfix/112-performance-chunked-decoding' into develop
Forward port #112
2 parents 4413d72 + b42d0b8 commit 9422140

File tree

3 files changed

+78
-8
lines changed

3 files changed

+78
-8
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ All notable changes to this project will be documented in this file, in reverse
4040

4141
### Fixed
4242

43+
- [#112](https://github.com/zendframework/zend-http/pull/112) provides performance improvements when parsing large chunked messages.
44+
4345
- introduces changes to `Response::fromString()` to pull the next line of the response
4446
and parse it for the status when a 100 status code is initially encountered, per https://tools.ietf.org/html/rfc7231\#section-6.2.1
4547

src/Response.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -524,17 +524,24 @@ protected function decodeChunkedBody($body)
524524
{
525525
$decBody = '';
526526

527-
while (trim($body)) {
528-
if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m)) {
529-
throw new Exception\RuntimeException(
530-
"Error parsing body - doesn't seem to be a chunked message"
531-
);
527+
$offset = 0;
528+
529+
while (true) {
530+
if (! preg_match("/^([\da-fA-F]+)[^\r\n]*\r\n/sm", $body, $m, 0, $offset)) {
531+
if (trim(substr($body, $offset))) {
532+
// Message was not consumed completely!
533+
throw new Exception\RuntimeException(
534+
'Error parsing body - doesn\'t seem to be a chunked message'
535+
);
536+
}
537+
// Message was consumed completely
538+
break;
532539
}
533540

534541
$length = hexdec(trim($m[1]));
535542
$cut = strlen($m[0]);
536-
$decBody .= substr($body, $cut, $length);
537-
$body = substr($body, $cut + $length + 2);
543+
$decBody .= substr($body, $offset + $cut, $length);
544+
$offset += $cut + $length + 2;
538545
}
539546

540547
return $decBody;

test/ResponseTest.php

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
33
* @see https://github.com/zendframework/zend-http for the canonical source repository
4-
* @copyright Copyright (c) 2005-2017 Zend Technologies USA Inc. (http://www.zend.com)
4+
* @copyright Copyright (c) 2005-2018 Zend Technologies USA Inc. (http://www.zend.com)
55
* @license https://github.com/zendframework/zend-http/blob/master/LICENSE.md New BSD License
66
*/
77

@@ -179,6 +179,67 @@ public function testChunkedResponseCaseInsensitiveZF5438()
179179
$this->assertEquals('c0cc9d44790fa2a58078059bab1902a9', md5($res->getContent()));
180180
}
181181

182+
/**
183+
* @param int $chunksize the data size of the chunk to create
184+
* @return string a chunk of data for embedding inside a chunked response
185+
*/
186+
private function makeChunk($chunksize)
187+
{
188+
return sprintf("%d\r\n%s\r\n", $chunksize, str_repeat('W', $chunksize));
189+
}
190+
191+
/**
192+
* @param Response $response
193+
* @return float the time that calling the getBody function took on the response
194+
*/
195+
private function getTimeForGetBody(Response $response)
196+
{
197+
$timeStart = microtime(true);
198+
$response->getBody();
199+
return microtime(true) - $timeStart;
200+
}
201+
202+
/**
203+
* @small
204+
*/
205+
public function testChunkedResponsePerformance()
206+
{
207+
$headers = new Headers();
208+
$headers->addHeaders([
209+
'Date' => 'Sun, 25 Jun 2006 19:55:19 GMT',
210+
'Server' => 'Apache',
211+
'X-powered-by' => 'PHP/5.1.4-pl3-gentoo',
212+
'Connection' => 'close',
213+
'Transfer-encoding' => 'chunked',
214+
'Content-type' => 'text/html',
215+
]);
216+
217+
$response = new Response();
218+
$response->setHeaders($headers);
219+
220+
// avoid flakiness, repeat test
221+
$timings = [];
222+
for ($i = 0; $i < 4; $i++) {
223+
// get baseline for timing: 2000 x 1 Byte chunks
224+
$responseData = str_repeat($this->makeChunk(1), 2000);
225+
$response->setContent($responseData);
226+
$time1 = $this->getTimeForGetBody($response);
227+
228+
// 'worst case' response, where 2000 1 Byte chunks are followed by a 10 MB Chunk
229+
$responseData2 = $responseData . $this->makeChunk(10000000);
230+
$response->setContent($responseData2);
231+
$time2 = $this->getTimeForGetBody($response);
232+
233+
$timings[] = floor($time2 / $time1);
234+
}
235+
236+
array_shift($timings); // do not measure first iteration
237+
238+
// make sure that the worst case packet will have an equal timing as the baseline
239+
$errMsg = 'Chunked response is not parsing large packets efficiently! Timings:';
240+
$this->assertLessThan(20, min($timings), $errMsg . print_r($timings, true));
241+
}
242+
182243
public function testLineBreaksCompatibility()
183244
{
184245
$responseTestLf = $this->readResponse('response_lfonly');

0 commit comments

Comments
 (0)