diff --git a/composer.json b/composer.json
index 69262e6..35db76d 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,8 @@
},
"require-dev": {
"phpspec/phpspec": "^2.4",
- "henrikbjorn/phpspec-code-coverage" : "^1.0"
+ "henrikbjorn/phpspec-code-coverage" : "^1.0",
+ "guzzlehttp/psr7": "^1.4"
},
"suggest": {
"php-http/logger-plugin": "PSR-3 Logger plugin",
diff --git a/spec/Plugin/ContentTypePluginSpec.php b/spec/Plugin/ContentTypePluginSpec.php
new file mode 100644
index 0000000..3df7d87
--- /dev/null
+++ b/spec/Plugin/ContentTypePluginSpec.php
@@ -0,0 +1,109 @@
+shouldHaveType('Http\Client\Common\Plugin\ContentTypePlugin');
+ }
+
+ function it_is_a_plugin()
+ {
+ $this->shouldImplement('Http\Client\Common\Plugin');
+ }
+
+ function it_adds_json_content_type_header(RequestInterface $request)
+ {
+ $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
+ $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for(json_encode(['foo' => 'bar'])));
+ $request->withHeader('Content-Type', 'application/json')->shouldBeCalled()->willReturn($request);
+
+ $this->handleRequest($request, function () {}, function () {});
+ }
+
+ function it_adds_xml_content_type_header(RequestInterface $request)
+ {
+ $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
+ $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar'));
+ $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request);
+
+ $this->handleRequest($request, function () {}, function () {});
+ }
+
+ function it_does_not_set_content_type_header(RequestInterface $request)
+ {
+ $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
+ $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo'));
+ $request->withHeader('Content-Type', null)->shouldNotBeCalled();
+
+ $this->handleRequest($request, function () {}, function () {});
+ }
+
+ function it_does_not_set_content_type_header_if_already_one(RequestInterface $request)
+ {
+ $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(true);
+ $request->getBody()->shouldNotBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('foo'));
+ $request->withHeader('Content-Type', null)->shouldNotBeCalled();
+
+ $this->handleRequest($request, function () {}, function () {});
+ }
+
+ function it_does_not_set_content_type_header_if_size_0_or_unknown(RequestInterface $request)
+ {
+ $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
+ $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for());
+ $request->withHeader('Content-Type', null)->shouldNotBeCalled();
+
+ $this->handleRequest($request, function () {}, function () {});
+ }
+
+ function it_adds_xml_content_type_header_if_size_limit_is_not_reached_using_default_value(RequestInterface $request)
+ {
+ $this->beConstructedWith([
+ 'skip_detection' => true
+ ]);
+
+ $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
+ $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar'));
+ $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request);
+
+ $this->handleRequest($request, function () {}, function () {});
+ }
+
+ function it_adds_xml_content_type_header_if_size_limit_is_not_reached(RequestInterface $request)
+ {
+ $this->beConstructedWith([
+ 'skip_detection' => true,
+ 'size_limit' => 32000000
+ ]);
+
+ $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
+ $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar'));
+ $request->withHeader('Content-Type', 'application/xml')->shouldBeCalled()->willReturn($request);
+
+ $this->handleRequest($request, function () {}, function () {});
+ }
+
+ function it_does_not_set_content_type_header_if_size_limit_is_reached(RequestInterface $request)
+ {
+ $this->beConstructedWith([
+ 'skip_detection' => true,
+ 'size_limit' => 8
+ ]);
+
+ $request->hasHeader('Content-Type')->shouldBeCalled()->willReturn(false);
+ $request->getBody()->shouldBeCalled()->willReturn(\GuzzleHttp\Psr7\stream_for('bar'));
+ $request->withHeader('Content-Type', null)->shouldNotBeCalled();
+
+ $this->handleRequest($request, function () {}, function () {});
+ }
+
+}
diff --git a/src/Plugin/ContentTypePlugin.php b/src/Plugin/ContentTypePlugin.php
new file mode 100644
index 0000000..038b3e4
--- /dev/null
+++ b/src/Plugin/ContentTypePlugin.php
@@ -0,0 +1,123 @@
+
+ */
+final class ContentTypePlugin implements Plugin
+{
+ /**
+ * Allow to disable the content type detection when stream is too large (as it can consume a lot of resource).
+ *
+ * @var bool
+ *
+ * true skip the content type detection
+ * false detect the content type (default value)
+ */
+ protected $skipDetection;
+
+ /**
+ * Determine the size stream limit for which the detection as to be skipped (default to 16Mb).
+ *
+ * @var int
+ */
+ protected $sizeLimit;
+
+ /**
+ * @param array $config {
+ *
+ * @var bool $skip_detection True skip detection if stream size is bigger than $size_limit.
+ * @var int $size_limit size stream limit for which the detection as to be skipped.
+ * }
+ */
+ public function __construct(array $config = [])
+ {
+ $resolver = new OptionsResolver();
+ $resolver->setDefaults([
+ 'skip_detection' => false,
+ 'size_limit' => 16000000,
+ ]);
+ $resolver->setAllowedTypes('skip_detection', 'bool');
+ $resolver->setAllowedTypes('size_limit', 'int');
+
+ $options = $resolver->resolve($config);
+
+ $this->skipDetection = $options['skip_detection'];
+ $this->sizeLimit = $options['size_limit'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function handleRequest(RequestInterface $request, callable $next, callable $first)
+ {
+ if (!$request->hasHeader('Content-Type')) {
+ $stream = $request->getBody();
+ $streamSize = $stream->getSize();
+
+ if (!$stream->isSeekable()) {
+ return $next($request);
+ }
+
+ if (0 === $streamSize) {
+ return $next($request);
+ }
+
+ if ($this->skipDetection && (null === $streamSize || $streamSize >= $this->sizeLimit)) {
+ return $next($request);
+ }
+
+ if ($this->isJson($stream)) {
+ $request = $request->withHeader('Content-Type', 'application/json');
+
+ return $next($request);
+ }
+
+ if ($this->isXml($stream)) {
+ $request = $request->withHeader('Content-Type', 'application/xml');
+
+ return $next($request);
+ }
+ }
+
+ return $next($request);
+ }
+
+ /**
+ * @param $stream StreamInterface
+ *
+ * @return bool
+ */
+ private function isJson($stream)
+ {
+ $stream->rewind();
+
+ json_decode($stream->getContents());
+
+ return json_last_error() == JSON_ERROR_NONE;
+ }
+
+ /**
+ * @param $stream StreamInterface
+ *
+ * @return \SimpleXMLElement|false
+ */
+ private function isXml($stream)
+ {
+ $stream->rewind();
+
+ $previousValue = libxml_use_internal_errors(true);
+ $isXml = simplexml_load_string($stream->getContents());
+ libxml_use_internal_errors($previousValue);
+
+ return $isXml;
+ }
+}