Skip to content

Commit 600ea9a

Browse files
kpn13Nyholm
authored andcommitted
Add Content-type plugin to set content-type header automatically (#85)
* Add Content-type plugin to set content-type header automatically (for now just JSON and XML) * Use guzzle/psr7 implementation for unit tests * Return early if body size is 0 or unknown * Reset the previous value at the end * Rewind stream before each content test * Add options to skip detection for too large stream * Skip detection if stream is not seekable * Add null comparison * Check if stream size is null only when skipDetection is true
1 parent 0f92f97 commit 600ea9a

File tree

3 files changed

+234
-1
lines changed

3 files changed

+234
-1
lines changed

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@
1919
},
2020
"require-dev": {
2121
"phpspec/phpspec": "^2.4",
22-
"henrikbjorn/phpspec-code-coverage" : "^1.0"
22+
"henrikbjorn/phpspec-code-coverage" : "^1.0",
23+
"guzzlehttp/psr7": "^1.4"
2324
},
2425
"suggest": {
2526
"php-http/logger-plugin": "PSR-3 Logger plugin",

spec/Plugin/ContentTypePluginSpec.php

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
namespace spec\Http\Client\Common\Plugin;
4+
5+
use PhpSpec\Exception\Example\SkippingException;
6+
use Psr\Http\Message\RequestInterface;
7+
use Psr\Http\Message\StreamInterface;
8+
use PhpSpec\ObjectBehavior;
9+
use Prophecy\Argument;
10+
11+
class ContentTypePluginSpec extends ObjectBehavior
12+
{
13+
function it_is_initializable()
14+
{
15+
$this->shouldHaveType('Http\Client\Common\Plugin\ContentTypePlugin');
16+
}
17+
18+
function it_is_a_plugin()
19+
{
20+
$this->shouldImplement('Http\Client\Common\Plugin');
21+
}
22+
23+
function it_adds_json_content_type_header(RequestInterface $request)
24+
{
25+
$request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
26+
$request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for(json_encode(['foo' => 'bar'])));
27+
$request->withHeader('Content-Type', 'application/json')->shouldBeCalled()->willReturn($request);
28+
29+
$this->handleRequest($request, function () {}, function () {});
30+
}
31+
32+
function it_adds_xml_content_type_header(RequestInterface $request)
33+
{
34+
$request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
35+
$request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('<foo>bar</foo>'));
36+
$request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request);
37+
38+
$this->handleRequest($request, function () {}, function () {});
39+
}
40+
41+
function it_does_not_set_content_type_header(RequestInterface $request)
42+
{
43+
$request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
44+
$request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo'));
45+
$request->withHeader('Content-Type', null)->shouldNotBeCalled();
46+
47+
$this->handleRequest($request, function () {}, function () {});
48+
}
49+
50+
function it_does_not_set_content_type_header_if_already_one(RequestInterface $request)
51+
{
52+
$request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(true);
53+
$request->getBody()->shouldNotBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo'));
54+
$request->withHeader('Content-Type', null)->shouldNotBeCalled();
55+
56+
$this->handleRequest($request, function () {}, function () {});
57+
}
58+
59+
function it_does_not_set_content_type_header_if_size_0_or_unknown(RequestInterface $request)
60+
{
61+
$request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
62+
$request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for());
63+
$request->withHeader('Content-Type', null)->shouldNotBeCalled();
64+
65+
$this->handleRequest($request, function () {}, function () {});
66+
}
67+
68+
function it_adds_xml_content_type_header_if_size_limit_is_not_reached_using_default_value(RequestInterface $request)
69+
{
70+
$this->beConstructedWith([
71+
'skip_detection' => true
72+
]);
73+
74+
$request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
75+
$request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('<foo>bar</foo>'));
76+
$request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request);
77+
78+
$this->handleRequest($request, function () {}, function () {});
79+
}
80+
81+
function it_adds_xml_content_type_header_if_size_limit_is_not_reached(RequestInterface $request)
82+
{
83+
$this->beConstructedWith([
84+
'skip_detection' => true,
85+
'size_limit' => 32000000
86+
]);
87+
88+
$request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
89+
$request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('<foo>bar</foo>'));
90+
$request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request);
91+
92+
$this->handleRequest($request, function () {}, function () {});
93+
}
94+
95+
function it_does_not_set_content_type_header_if_size_limit_is_reached(RequestInterface $request)
96+
{
97+
$this->beConstructedWith([
98+
'skip_detection' => true,
99+
'size_limit' => 8
100+
]);
101+
102+
$request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
103+
$request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('<foo>bar</foo>'));
104+
$request->withHeader('Content-Type', null)->shouldNotBeCalled();
105+
106+
$this->handleRequest($request, function () {}, function () {});
107+
}
108+
109+
}

src/Plugin/ContentTypePlugin.php

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
namespace Http\Client\Common\Plugin;
4+
5+
use Http\Client\Common\Plugin;
6+
use Psr\Http\Message\RequestInterface;
7+
use Psr\Http\Message\StreamInterface;
8+
use Symfony\Component\OptionsResolver\OptionsResolver;
9+
10+
/**
11+
* Allow to set the correct content type header on the request automatically only if it is not set.
12+
*
13+
* @author Karim Pinchon <[email protected]>
14+
*/
15+
final class ContentTypePlugin implements Plugin
16+
{
17+
/**
18+
* Allow to disable the content type detection when stream is too large (as it can consume a lot of resource).
19+
*
20+
* @var bool
21+
*
22+
* true skip the content type detection
23+
* false detect the content type (default value)
24+
*/
25+
protected $skipDetection;
26+
27+
/**
28+
* Determine the size stream limit for which the detection as to be skipped (default to 16Mb).
29+
*
30+
* @var int
31+
*/
32+
protected $sizeLimit;
33+
34+
/**
35+
* @param array $config {
36+
*
37+
* @var bool $skip_detection True skip detection if stream size is bigger than $size_limit.
38+
* @var int $size_limit size stream limit for which the detection as to be skipped.
39+
* }
40+
*/
41+
public function __construct(array $config = [])
42+
{
43+
$resolver = new OptionsResolver();
44+
$resolver->setDefaults([
45+
'skip_detection' => false,
46+
'size_limit' => 16000000,
47+
]);
48+
$resolver->setAllowedTypes('skip_detection', 'bool');
49+
$resolver->setAllowedTypes('size_limit', 'int');
50+
51+
$options = $resolver->resolve($config);
52+
53+
$this->skipDetection = $options['skip_detection'];
54+
$this->sizeLimit = $options['size_limit'];
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function handleRequest(RequestInterface $request, callable $next, callable $first)
61+
{
62+
if (!$request->hasHeader('Content-Type')) {
63+
$stream = $request->getBody();
64+
$streamSize = $stream->getSize();
65+
66+
if (!$stream->isSeekable()) {
67+
return $next($request);
68+
}
69+
70+
if (0 === $streamSize) {
71+
return $next($request);
72+
}
73+
74+
if ($this->skipDetection && (null === $streamSize || $streamSize >= $this->sizeLimit)) {
75+
return $next($request);
76+
}
77+
78+
if ($this->isJson($stream)) {
79+
$request = $request->withHeader('Content-Type', 'application/json');
80+
81+
return $next($request);
82+
}
83+
84+
if ($this->isXml($stream)) {
85+
$request = $request->withHeader('Content-Type', 'application/xml');
86+
87+
return $next($request);
88+
}
89+
}
90+
91+
return $next($request);
92+
}
93+
94+
/**
95+
* @param $stream StreamInterface
96+
*
97+
* @return bool
98+
*/
99+
private function isJson($stream)
100+
{
101+
$stream->rewind();
102+
103+
json_decode($stream->getContents());
104+
105+
return json_last_error() == JSON_ERROR_NONE;
106+
}
107+
108+
/**
109+
* @param $stream StreamInterface
110+
*
111+
* @return \SimpleXMLElement|false
112+
*/
113+
private function isXml($stream)
114+
{
115+
$stream->rewind();
116+
117+
$previousValue = libxml_use_internal_errors(true);
118+
$isXml = simplexml_load_string($stream->getContents());
119+
libxml_use_internal_errors($previousValue);
120+
121+
return $isXml;
122+
}
123+
}

0 commit comments

Comments
 (0)