From 62f3cfcc7446eb164f77fd0a036086900783c68c Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Fri, 13 Nov 2015 01:50:48 +0100 Subject: [PATCH 1/4] Add decode plugin --- composer.json | 6 +- spec/DecoderPluginSpec.php | 143 +++++++++++++++++++++++++++++++++++++ spec/EncoderPluginSpec.php | 15 ++++ src/DecoderPlugin.php | 132 ++++++++++++++++++++++++++++++++++ src/EncoderPlugin.php | 21 ++++++ 5 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 spec/DecoderPluginSpec.php create mode 100644 spec/EncoderPluginSpec.php create mode 100644 src/DecoderPlugin.php create mode 100644 src/EncoderPlugin.php diff --git a/composer.json b/composer.json index 53e906d..38b3567 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "php-http/authentication": "^0.1@dev", "php-http/cookie": "^0.1@dev", "symfony/stopwatch": "^2.3", - "psr/log": "^1.0" + "psr/log": "^1.0", + "php-http/encoding": "^0.1@dev" }, "autoload": { "psr-4": { @@ -32,7 +33,8 @@ "php-http/authentication": "Allow to use the AuthenticationPlugin", "php-http/cookie": "Allow to use CookiePlugin", "symfony/stopwatch": "Allow to use the StopwatchPlugin", - "psr/log-implementation": "Allow to use the LoggerPlugin" + "psr/log-implementation": "Allow to use the LoggerPlugin", + "php-http/encoding": "Allow to use the Decoder and Encoder plugin" }, "scripts": { "test": "vendor/bin/phpspec run", diff --git a/spec/DecoderPluginSpec.php b/spec/DecoderPluginSpec.php new file mode 100644 index 0000000..272ecb2 --- /dev/null +++ b/spec/DecoderPluginSpec.php @@ -0,0 +1,143 @@ +shouldHaveType('Http\Client\Plugin\DecoderPlugin'); + $this->shouldImplement('Http\Client\Plugin\Plugin'); + } + + function it_decodes(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + { + $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); + $next = function () use($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(true); + $response->getHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(['chunked']); + $response->getBody()->shouldBeCalled()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\DechunkStream'))->shouldBeCalled()->willReturn($response); + $response->withHeader('Transfer-Encoding', [])->shouldBeCalled()->willReturn($response); + $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(false); + + $stream->isReadable()->shouldBeCalled()->willReturn(true); + $stream->isWritable()->shouldBeCalled()->willReturn(false); + $stream->eof()->shouldBeCalled()->willReturn(false); + + $this->handleRequest($request, $next, function () {}); + } + + function it_decodes_in_exception_with_a_response(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + { + $exception = new HttpException('', $request->getWrappedObject(), $response->getWrappedObject()); + $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); + $next = function () use($exception) { + return new RejectedPromise($exception); + }; + + $response->getStatusCode()->shouldBeCalled()->willReturn(404); + $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(true); + $response->getHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(['chunked']); + $response->getBody()->shouldBeCalled()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\DechunkStream'))->shouldBeCalled()->willReturn($response); + $response->withHeader('Transfer-Encoding', [])->shouldBeCalled()->willReturn($response); + $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(false); + + $stream->isReadable()->shouldBeCalled()->willReturn(true); + $stream->isWritable()->shouldBeCalled()->willReturn(false); + $stream->eof()->shouldBeCalled()->willReturn(false); + + $this->handleRequest($request, $next, function () {}); + } + + function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + { + $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); + $next = function () use($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(false); + $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(true); + $response->getHeader('Content-Encoding')->shouldBeCalled()->willReturn(['gzip']); + $response->getBody()->shouldBeCalled()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\GzipDecodeStream'))->shouldBeCalled()->willReturn($response); + $response->withHeader('Content-Encoding', [])->shouldBeCalled()->willReturn($response); + + $stream->isReadable()->shouldBeCalled()->willReturn(true); + $stream->isWritable()->shouldBeCalled()->willReturn(false); + $stream->eof()->shouldBeCalled()->willReturn(false); + + $this->handleRequest($request, $next, function () {}); + } + + function it_decodes_deflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + { + $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); + $next = function () use($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(false); + $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(true); + $response->getHeader('Content-Encoding')->shouldBeCalled()->willReturn(['deflate']); + $response->getBody()->shouldBeCalled()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\InflateStream'))->shouldBeCalled()->willReturn($response); + $response->withHeader('Content-Encoding', [])->shouldBeCalled()->willReturn($response); + + $stream->isReadable()->shouldBeCalled()->willReturn(true); + $stream->isWritable()->shouldBeCalled()->willReturn(false); + $stream->eof()->shouldBeCalled()->willReturn(false); + + $this->handleRequest($request, $next, function () {}); + } + + function it_decodes_inflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) + { + $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); + $next = function () use($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(false); + $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(true); + $response->getHeader('Content-Encoding')->shouldBeCalled()->willReturn(['compress']); + $response->getBody()->shouldBeCalled()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\DecompressStream'))->shouldBeCalled()->willReturn($response); + $response->withHeader('Content-Encoding', [])->shouldBeCalled()->willReturn($response); + + $stream->isReadable()->shouldBeCalled()->willReturn(true); + $stream->isWritable()->shouldBeCalled()->willReturn(false); + $stream->eof()->shouldBeCalled()->willReturn(false); + + $this->handleRequest($request, $next, function () {}); + } + + function it_does_not_decode_with_content_encoding(RequestInterface $request, ResponseInterface $response) + { + $this->beConstructedWith(false); + + $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldNotBeCalled(); + $next = function () use($response) { + return new FulfilledPromise($response->getWrappedObject()); + }; + + $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(false); + $response->hasHeader('Content-Encoding')->shouldNotBeCalled(); + + $this->handleRequest($request, $next, function () {}); + } +} diff --git a/spec/EncoderPluginSpec.php b/spec/EncoderPluginSpec.php new file mode 100644 index 0000000..79c350f --- /dev/null +++ b/spec/EncoderPluginSpec.php @@ -0,0 +1,15 @@ +shouldHaveType('Http\Client\Plugin\EncoderPlugin'); + $this->shouldImplement('Http\Client\Plugin\Plugin'); + } +} diff --git a/src/DecoderPlugin.php b/src/DecoderPlugin.php new file mode 100644 index 0000000..e60a3e5 --- /dev/null +++ b/src/DecoderPlugin.php @@ -0,0 +1,132 @@ + + */ +class DecoderPlugin implements Plugin +{ + /** + * @var bool Whether this plugin decode stream with value in the Content-Encoding header (default to true). + * + * If set to false only the Transfer-Encoding header will be used. + */ + private $useContentEncoding; + + public function __construct($useContentEncoding = true) + { + $this->useContentEncoding = $useContentEncoding; + } + + /** + * {@inheritDoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first) + { + if ($this->useContentEncoding) { + $request = $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress']); + } + + return $next($request)->then(function (ResponseInterface $response) { + return $this->decodeResponse($response); + }, function (Exception $exception) use($request) { + if ($exception instanceof Exception\HttpException) { + $response = $this->decodeResponse($exception->getResponse()); + $exception = new Exception\HttpException($exception->getMessage(), $request, $response, $exception); + } + + throw $exception; + }); + } + + /** + * Decode a response body given its Transfer-Encoding or Content-Encoding value + * + * @param ResponseInterface $response Response to decode + * + * @return ResponseInterface New response decoded + */ + protected function decodeResponse(ResponseInterface $response) + { + if ($response->hasHeader('Transfer-Encoding')) { + $encodings = $response->getHeader('Transfer-Encoding'); + $newEncodings = []; + + while ($encoding = array_pop($encodings)) { + $stream = $this->decorateStream($encoding, $response->getBody()); + + if (false === $stream) { + array_unshift($newEncodings, $encoding); + + continue; + } + + $response = $response->withBody($stream); + } + + $response = $response->withHeader('Transfer-Encoding', $newEncodings); + } + + if ($this->useContentEncoding && $response->hasHeader('Content-Encoding')) { + $encodings = $response->getHeader('Content-Encoding'); + $newEncodings = []; + + while ($encoding = array_pop($encodings)) { + $stream = $this->decorateStream($encoding, $response->getBody()); + + if (false === $stream) { + array_unshift($newEncodings, $encoding); + + continue; + } + + $response = $response->withBody($stream); + } + + $response = $response->withHeader('Content-Encoding', $newEncodings); + } + + return $response; + } + + /** + * Decorate a stream given an encoding + * + * @param string $encoding + * @param StreamInterface $stream + * + * @return StreamInterface|false A new stream interface or false if encoding is not supported + */ + protected function decorateStream($encoding, StreamInterface $stream) + { + if (strtolower($encoding) == 'chunked') { + return new DechunkStream($stream); + } + + if (strtolower($encoding) == 'compress') { + return new DecompressStream($stream); + } + + if (strtolower($encoding) == 'deflate') { + return new InflateStream($stream); + } + + if (strtolower($encoding) == 'gzip') { + return new GzipDecodeStream($stream); + } + + return false; + } +} diff --git a/src/EncoderPlugin.php b/src/EncoderPlugin.php new file mode 100644 index 0000000..bab281b --- /dev/null +++ b/src/EncoderPlugin.php @@ -0,0 +1,21 @@ + + */ +class EncoderPlugin implements Plugin +{ + /** + * {@inheritDoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first) + { + // TODO: Implement handleRequest() method. + } +} From 4a3378a412ce8335e8175fc38bfaa172840da368 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Fri, 13 Nov 2015 10:23:33 +0100 Subject: [PATCH 2/4] Add content length plugin --- .travis.yml | 2 +- spec/ContentLengthPluginSpec.php | 42 +++++++++++ spec/DecoderPluginSpec.php | 117 ++++++++++++++----------------- spec/EncoderPluginSpec.php | 15 ---- src/ContentLengthPlugin.php | 34 +++++++++ src/DecoderPlugin.php | 56 +++++++-------- src/EncoderPlugin.php | 21 ------ 7 files changed, 158 insertions(+), 129 deletions(-) create mode 100644 spec/ContentLengthPluginSpec.php delete mode 100644 spec/EncoderPluginSpec.php create mode 100644 src/ContentLengthPlugin.php delete mode 100644 src/EncoderPlugin.php diff --git a/.travis.yml b/.travis.yml index 2a34ac2..c088c60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ env: matrix: allow_failures: - - php: 7.0 + - php: hhvm fast_finish: true include: - php: 5.4 diff --git a/spec/ContentLengthPluginSpec.php b/spec/ContentLengthPluginSpec.php new file mode 100644 index 0000000..9c0af6b --- /dev/null +++ b/spec/ContentLengthPluginSpec.php @@ -0,0 +1,42 @@ +shouldHaveType('Http\Client\Plugin\ContentLengthPlugin'); + $this->shouldImplement('Http\Client\Plugin\Plugin'); + } + + function it_adds_content_length_header(RequestInterface $request, StreamInterface $stream) + { + $request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn($stream); + $stream->getSize()->shouldBeCalled()->willReturn(100); + $request->withHeader('Content-Length', 100)->shouldBeCalled()->willReturn($request); + + $this->handleRequest($request, function () {}, function () {}); + } + + function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterface $stream) + { + $request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false); + $request->getBody()->shouldBeCalled()->willReturn($stream); + + $stream->getSize()->shouldBeCalled()->willReturn(null); + $stream->isReadable()->shouldBeCalled()->willReturn(true); + $stream->isWritable()->shouldBeCalled()->willReturn(false); + $stream->eof()->shouldBeCalled()->willReturn(false); + + $request->withBody(Argument::type('Http\Encoding\ChunkStream'))->shouldBeCalled()->willReturn($request); + + $this->handleRequest($request, function () {}, function () {}); + } +} diff --git a/spec/DecoderPluginSpec.php b/spec/DecoderPluginSpec.php index 272ecb2..43376db 100644 --- a/spec/DecoderPluginSpec.php +++ b/spec/DecoderPluginSpec.php @@ -2,126 +2,114 @@ namespace spec\Http\Client\Plugin; -use Http\Client\Exception\HttpException; use Http\Client\Utils\Promise\FulfilledPromise; -use Http\Client\Utils\Promise\RejectedPromise; -use PhpSpec\ObjectBehavior; -use Prophecy\Argument; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; +use PhpSpec\Exception\Example\SkippingException; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; class DecoderPluginSpec extends ObjectBehavior { function it_is_initializable() { $this->shouldHaveType('Http\Client\Plugin\DecoderPlugin'); + } + + function it_is_a_plugin() + { $this->shouldImplement('Http\Client\Plugin\Plugin'); } function it_decodes(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { + if(defined('HHVM_VERSION')) { + throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm'); + } + + $request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); $next = function () use($response) { return new FulfilledPromise($response->getWrappedObject()); }; - $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(true); - $response->getHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(['chunked']); - $response->getBody()->shouldBeCalled()->willReturn($stream); - $response->withBody(Argument::type('Http\Encoding\DechunkStream'))->shouldBeCalled()->willReturn($response); - $response->withHeader('Transfer-Encoding', [])->shouldBeCalled()->willReturn($response); - $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(false); - - $stream->isReadable()->shouldBeCalled()->willReturn(true); - $stream->isWritable()->shouldBeCalled()->willReturn(false); - $stream->eof()->shouldBeCalled()->willReturn(false); - - $this->handleRequest($request, $next, function () {}); - } - - function it_decodes_in_exception_with_a_response(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) - { - $exception = new HttpException('', $request->getWrappedObject(), $response->getWrappedObject()); - $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); - $next = function () use($exception) { - return new RejectedPromise($exception); - }; - - $response->getStatusCode()->shouldBeCalled()->willReturn(404); - $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(true); - $response->getHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(['chunked']); - $response->getBody()->shouldBeCalled()->willReturn($stream); - $response->withBody(Argument::type('Http\Encoding\DechunkStream'))->shouldBeCalled()->willReturn($response); - $response->withHeader('Transfer-Encoding', [])->shouldBeCalled()->willReturn($response); - $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(false); + $response->hasHeader('Transfer-Encoding')->willReturn(true); + $response->getHeader('Transfer-Encoding')->willReturn(['chunked']); + $response->getBody()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\DechunkStream'))->willReturn($response); + $response->withHeader('Transfer-Encoding', [])->willReturn($response); + $response->hasHeader('Content-Encoding')->willReturn(false); - $stream->isReadable()->shouldBeCalled()->willReturn(true); - $stream->isWritable()->shouldBeCalled()->willReturn(false); - $stream->eof()->shouldBeCalled()->willReturn(false); + $stream->isReadable()->willReturn(true); + $stream->isWritable()->willReturn(false); + $stream->eof()->willReturn(false); $this->handleRequest($request, $next, function () {}); } function it_decodes_gzip(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { + $request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); $next = function () use($response) { return new FulfilledPromise($response->getWrappedObject()); }; - $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(false); - $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(true); - $response->getHeader('Content-Encoding')->shouldBeCalled()->willReturn(['gzip']); - $response->getBody()->shouldBeCalled()->willReturn($stream); - $response->withBody(Argument::type('Http\Encoding\GzipDecodeStream'))->shouldBeCalled()->willReturn($response); - $response->withHeader('Content-Encoding', [])->shouldBeCalled()->willReturn($response); + $response->hasHeader('Transfer-Encoding')->willReturn(false); + $response->hasHeader('Content-Encoding')->willReturn(true); + $response->getHeader('Content-Encoding')->willReturn(['gzip']); + $response->getBody()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\GzipDecodeStream'))->willReturn($response); + $response->withHeader('Content-Encoding', [])->willReturn($response); - $stream->isReadable()->shouldBeCalled()->willReturn(true); - $stream->isWritable()->shouldBeCalled()->willReturn(false); - $stream->eof()->shouldBeCalled()->willReturn(false); + $stream->isReadable()->willReturn(true); + $stream->isWritable()->willReturn(false); + $stream->eof()->willReturn(false); $this->handleRequest($request, $next, function () {}); } function it_decodes_deflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { + $request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); $next = function () use($response) { return new FulfilledPromise($response->getWrappedObject()); }; - $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(false); - $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(true); - $response->getHeader('Content-Encoding')->shouldBeCalled()->willReturn(['deflate']); - $response->getBody()->shouldBeCalled()->willReturn($stream); - $response->withBody(Argument::type('Http\Encoding\InflateStream'))->shouldBeCalled()->willReturn($response); - $response->withHeader('Content-Encoding', [])->shouldBeCalled()->willReturn($response); + $response->hasHeader('Transfer-Encoding')->willReturn(false); + $response->hasHeader('Content-Encoding')->willReturn(true); + $response->getHeader('Content-Encoding')->willReturn(['deflate']); + $response->getBody()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\InflateStream'))->willReturn($response); + $response->withHeader('Content-Encoding', [])->willReturn($response); - $stream->isReadable()->shouldBeCalled()->willReturn(true); - $stream->isWritable()->shouldBeCalled()->willReturn(false); - $stream->eof()->shouldBeCalled()->willReturn(false); + $stream->isReadable()->willReturn(true); + $stream->isWritable()->willReturn(false); + $stream->eof()->willReturn(false); $this->handleRequest($request, $next, function () {}); } function it_decodes_inflate(RequestInterface $request, ResponseInterface $response, StreamInterface $stream) { + $request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldBeCalled()->willReturn($request); $next = function () use($response) { return new FulfilledPromise($response->getWrappedObject()); }; - $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(false); - $response->hasHeader('Content-Encoding')->shouldBeCalled()->willReturn(true); - $response->getHeader('Content-Encoding')->shouldBeCalled()->willReturn(['compress']); - $response->getBody()->shouldBeCalled()->willReturn($stream); - $response->withBody(Argument::type('Http\Encoding\DecompressStream'))->shouldBeCalled()->willReturn($response); - $response->withHeader('Content-Encoding', [])->shouldBeCalled()->willReturn($response); + $response->hasHeader('Transfer-Encoding')->willReturn(false); + $response->hasHeader('Content-Encoding')->willReturn(true); + $response->getHeader('Content-Encoding')->willReturn(['compress']); + $response->getBody()->willReturn($stream); + $response->withBody(Argument::type('Http\Encoding\DecompressStream'))->willReturn($response); + $response->withHeader('Content-Encoding', [])->willReturn($response); - $stream->isReadable()->shouldBeCalled()->willReturn(true); - $stream->isWritable()->shouldBeCalled()->willReturn(false); - $stream->eof()->shouldBeCalled()->willReturn(false); + $stream->isReadable()->willReturn(true); + $stream->isWritable()->willReturn(false); + $stream->eof()->willReturn(false); $this->handleRequest($request, $next, function () {}); } @@ -130,12 +118,13 @@ function it_does_not_decode_with_content_encoding(RequestInterface $request, Res { $this->beConstructedWith(false); + $request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked'])->shouldBeCalled()->willReturn($request); $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress'])->shouldNotBeCalled(); $next = function () use($response) { return new FulfilledPromise($response->getWrappedObject()); }; - $response->hasHeader('Transfer-Encoding')->shouldBeCalled()->willReturn(false); + $response->hasHeader('Transfer-Encoding')->willReturn(false); $response->hasHeader('Content-Encoding')->shouldNotBeCalled(); $this->handleRequest($request, $next, function () {}); diff --git a/spec/EncoderPluginSpec.php b/spec/EncoderPluginSpec.php deleted file mode 100644 index 79c350f..0000000 --- a/spec/EncoderPluginSpec.php +++ /dev/null @@ -1,15 +0,0 @@ -shouldHaveType('Http\Client\Plugin\EncoderPlugin'); - $this->shouldImplement('Http\Client\Plugin\Plugin'); - } -} diff --git a/src/ContentLengthPlugin.php b/src/ContentLengthPlugin.php new file mode 100644 index 0000000..afab963 --- /dev/null +++ b/src/ContentLengthPlugin.php @@ -0,0 +1,34 @@ + + */ +class ContentLengthPlugin implements Plugin +{ + /** + * {@inheritDoc} + */ + public function handleRequest(RequestInterface $request, callable $next, callable $first) + { + if (!$request->hasHeader('Content-Length')) { + $stream = $request->getBody(); + + // Cannot determine the size so we use a chunk stream + if (null === $stream->getSize()) { + $stream = new ChunkStream($stream); + $request = $request->withBody($stream); + } else { + $request = $request->withHeader('Content-Length', $stream->getSize()); + } + } + + return $next($request); + } +} diff --git a/src/DecoderPlugin.php b/src/DecoderPlugin.php index e60a3e5..bc844b2 100644 --- a/src/DecoderPlugin.php +++ b/src/DecoderPlugin.php @@ -25,6 +25,11 @@ class DecoderPlugin implements Plugin */ private $useContentEncoding; + /** + * @param bool $useContentEncoding Whether this plugin decode stream with value in the Content-Encoding header (default to true). + * + * If set to false only the Transfer-Encoding header will be used. + */ public function __construct($useContentEncoding = true) { $this->useContentEncoding = $useContentEncoding; @@ -35,19 +40,14 @@ public function __construct($useContentEncoding = true) */ public function handleRequest(RequestInterface $request, callable $next, callable $first) { + $request = $request->withHeader('TE', ['gzip', 'deflate', 'compress', 'chunked']); + if ($this->useContentEncoding) { $request = $request->withHeader('Accept-Encoding', ['gzip', 'deflate', 'compress']); } return $next($request)->then(function (ResponseInterface $response) { return $this->decodeResponse($response); - }, function (Exception $exception) use($request) { - if ($exception instanceof Exception\HttpException) { - $response = $this->decodeResponse($exception->getResponse()); - $exception = new Exception\HttpException($exception->getMessage(), $request, $response, $exception); - } - - throw $exception; }); } @@ -58,29 +58,29 @@ public function handleRequest(RequestInterface $request, callable $next, callabl * * @return ResponseInterface New response decoded */ - protected function decodeResponse(ResponseInterface $response) + private function decodeResponse(ResponseInterface $response) { - if ($response->hasHeader('Transfer-Encoding')) { - $encodings = $response->getHeader('Transfer-Encoding'); - $newEncodings = []; + $response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response); - while ($encoding = array_pop($encodings)) { - $stream = $this->decorateStream($encoding, $response->getBody()); - - if (false === $stream) { - array_unshift($newEncodings, $encoding); - - continue; - } - - $response = $response->withBody($stream); - } - - $response = $response->withHeader('Transfer-Encoding', $newEncodings); + if ($this->useContentEncoding) { + $response = $this->decodeOnEncodingHeader('Content-Encoding', $response); } - if ($this->useContentEncoding && $response->hasHeader('Content-Encoding')) { - $encodings = $response->getHeader('Content-Encoding'); + return $response; + } + + /** + * Decode a response on a specific header (content encoding or transfer encoding mainly) + * + * @param string $headerName Name of the header + * @param ResponseInterface $response Response + * + * @return ResponseInterface A new instance of the response decoded + */ + private function decodeOnEncodingHeader($headerName, ResponseInterface $response) + { + if ($response->hasHeader($headerName)) { + $encodings = $response->getHeader($headerName); $newEncodings = []; while ($encoding = array_pop($encodings)) { @@ -95,7 +95,7 @@ protected function decodeResponse(ResponseInterface $response) $response = $response->withBody($stream); } - $response = $response->withHeader('Content-Encoding', $newEncodings); + $response = $response->withHeader($headerName, $newEncodings); } return $response; @@ -109,7 +109,7 @@ protected function decodeResponse(ResponseInterface $response) * * @return StreamInterface|false A new stream interface or false if encoding is not supported */ - protected function decorateStream($encoding, StreamInterface $stream) + private function decorateStream($encoding, StreamInterface $stream) { if (strtolower($encoding) == 'chunked') { return new DechunkStream($stream); diff --git a/src/EncoderPlugin.php b/src/EncoderPlugin.php deleted file mode 100644 index bab281b..0000000 --- a/src/EncoderPlugin.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ -class EncoderPlugin implements Plugin -{ - /** - * {@inheritDoc} - */ - public function handleRequest(RequestInterface $request, callable $next, callable $first) - { - // TODO: Implement handleRequest() method. - } -} From 3a606b91883ba1e444032393c56db4b837d949b3 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 18 Nov 2015 01:20:33 +0100 Subject: [PATCH 3/4] Skip failing test on HHVM --- spec/ContentLengthPluginSpec.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/ContentLengthPluginSpec.php b/spec/ContentLengthPluginSpec.php index 9c0af6b..b0a683f 100644 --- a/spec/ContentLengthPluginSpec.php +++ b/spec/ContentLengthPluginSpec.php @@ -2,6 +2,7 @@ namespace spec\Http\Client\Plugin; +use PhpSpec\Exception\Example\SkippingException; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Psr\Http\Message\RequestInterface; @@ -27,6 +28,10 @@ function it_adds_content_length_header(RequestInterface $request, StreamInterfac function it_streams_chunked_if_no_size(RequestInterface $request, StreamInterface $stream) { + if(defined('HHVM_VERSION')) { + throw new SkippingException('Skipping test on hhvm, as there is no chunk encoding on hhvm'); + } + $request->hasHeader('Content-Length')->shouldBeCalled()->willReturn(false); $request->getBody()->shouldBeCalled()->willReturn($stream); From 1f37656f5b9acac212bb44ff63b6dafeec6702dc Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 18 Nov 2015 11:13:01 +0100 Subject: [PATCH 4/4] Remove hhvm from allowed failures --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c088c60..33db299 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,8 +18,6 @@ env: - TEST_COMMAND="composer test" matrix: - allow_failures: - - php: hhvm fast_finish: true include: - php: 5.4