-
Notifications
You must be signed in to change notification settings - Fork 6
Decoder and ContentLength plugin #9
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<?php | ||
|
||
namespace spec\Http\Client\Plugin; | ||
|
||
use PhpSpec\Exception\Example\SkippingException; | ||
use PhpSpec\ObjectBehavior; | ||
use Prophecy\Argument; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\StreamInterface; | ||
|
||
class ContentLengthPluginSpec extends ObjectBehavior | ||
{ | ||
function it_is_initializable() | ||
{ | ||
$this->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) | ||
{ | ||
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); | ||
|
||
$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 () {}); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
<?php | ||
|
||
namespace spec\Http\Client\Plugin; | ||
|
||
use Http\Client\Utils\Promise\FulfilledPromise; | ||
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')->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()->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')->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()->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')->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()->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')->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()->willReturn(true); | ||
$stream->isWritable()->willReturn(false); | ||
$stream->eof()->willReturn(false); | ||
|
||
$this->handleRequest($request, $next, function () {}); | ||
} | ||
|
||
function it_does_not_decode_with_content_encoding(RequestInterface $request, ResponseInterface $response) | ||
{ | ||
$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')->willReturn(false); | ||
$response->hasHeader('Content-Encoding')->shouldNotBeCalled(); | ||
|
||
$this->handleRequest($request, $next, function () {}); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
namespace Http\Client\Plugin; | ||
|
||
use Http\Encoding\ChunkStream; | ||
use Psr\Http\Message\RequestInterface; | ||
|
||
/** | ||
* Allow to set the correct content length header on the request or to transfer it as a chunk if not possible | ||
* | ||
* @author Joel Wurtz <[email protected]> | ||
*/ | ||
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); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
<?php | ||
|
||
namespace Http\Client\Plugin; | ||
|
||
use Http\Client\Exception; | ||
use Http\Encoding\DechunkStream; | ||
use Http\Encoding\DecompressStream; | ||
use Http\Encoding\GzipDecodeStream; | ||
use Http\Encoding\InflateStream; | ||
use Psr\Http\Message\RequestInterface; | ||
use Psr\Http\Message\ResponseInterface; | ||
use Psr\Http\Message\StreamInterface; | ||
|
||
/** | ||
* Allow to decode response body with a chunk, deflate, compress or gzip encoding | ||
* | ||
* @author Joel Wurtz <[email protected]> | ||
*/ | ||
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; | ||
|
||
/** | ||
* @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; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
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); | ||
}); | ||
} | ||
|
||
/** | ||
* Decode a response body given its Transfer-Encoding or Content-Encoding value | ||
* | ||
* @param ResponseInterface $response Response to decode | ||
* | ||
* @return ResponseInterface New response decoded | ||
*/ | ||
private function decodeResponse(ResponseInterface $response) | ||
{ | ||
$response = $this->decodeOnEncodingHeader('Transfer-Encoding', $response); | ||
|
||
if ($this->useContentEncoding) { | ||
$response = $this->decodeOnEncodingHeader('Content-Encoding', $response); | ||
} | ||
|
||
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)) { | ||
$stream = $this->decorateStream($encoding, $response->getBody()); | ||
|
||
if (false === $stream) { | ||
array_unshift($newEncodings, $encoding); | ||
|
||
continue; | ||
} | ||
|
||
$response = $response->withBody($stream); | ||
} | ||
|
||
$response = $response->withHeader($headerName, $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 | ||
*/ | ||
private 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; | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
duplication alert. can we move this to a private method instead?
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.
Removed