feature #35407 [HttpClient] collect the body of responses when possible (nicolas-grekas)
This PR was merged into the 5.1-dev branch.
Discussion
----------
[HttpClient] collect the body of responses when possible
| Q | A
| ------------- | ---
| Branch? | master
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Tickets | Part of #33311
| License | MIT
| Doc PR | -
This is missing one thing: the HTML part in the profiler.
![image](https://user-images.githubusercontent.com/243674/72798816-29813e00-3c44-11ea-9586-99c2c6b91640.png)
![image](https://user-images.githubusercontent.com/243674/72798851-3f8efe80-3c44-11ea-973b-7ecc64a5a542.png)
Commits
-------
121f72839c
[HttpClient] collect the body of responses when possible
This commit is contained in:
commit
07818f2747
@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request;
|
|||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
|
||||||
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
|
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
|
||||||
|
use Symfony\Component\VarDumper\Caster\ImgStub;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Jérémy Romey <jeremy@free-agent.fr>
|
* @author Jérémy Romey <jeremy@free-agent.fr>
|
||||||
@ -128,8 +129,29 @@ final class HttpClientDataCollector extends DataCollector implements LateDataCol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (\is_string($content = $trace['content'])) {
|
||||||
|
$contentType = 'application/octet-stream';
|
||||||
|
|
||||||
|
foreach ($info['response_headers'] ?? [] as $h) {
|
||||||
|
if (0 === stripos($h, 'content-type: ')) {
|
||||||
|
$contentType = substr($h, \strlen('content-type: '));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (0 === strpos($contentType, 'image/') && class_exists(ImgStub::class)) {
|
||||||
|
$content = new ImgStub($content, $contentType, '');
|
||||||
|
} else {
|
||||||
|
$content = [$content];
|
||||||
|
}
|
||||||
|
|
||||||
|
$k = 'response_content';
|
||||||
|
} else {
|
||||||
|
$k = 'response_json';
|
||||||
|
}
|
||||||
|
|
||||||
$debugInfo = array_diff_key($info, $baseInfo);
|
$debugInfo = array_diff_key($info, $baseInfo);
|
||||||
$info = array_diff_key($info, $debugInfo) + ['debug_info' => $debugInfo];
|
$info = ['info' => $debugInfo] + array_diff_key($info, $debugInfo) + [$k => $content];
|
||||||
unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
|
unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
|
||||||
$traces[$i]['info'] = $this->cloneVar($info);
|
$traces[$i]['info'] = $this->cloneVar($info);
|
||||||
$traces[$i]['options'] = $this->cloneVar($trace['options']);
|
$traces[$i]['options'] = $this->cloneVar($trace['options']);
|
||||||
|
122
src/Symfony/Component/HttpClient/Response/TraceableResponse.php
Normal file
122
src/Symfony/Component/HttpClient/Response/TraceableResponse.php
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpClient\Response;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||||
|
use Symfony\Component\HttpClient\Exception\RedirectionException;
|
||||||
|
use Symfony\Component\HttpClient\Exception\ServerException;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nicolas Grekas <p@tchwork.com>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class TraceableResponse implements ResponseInterface
|
||||||
|
{
|
||||||
|
private $client;
|
||||||
|
private $response;
|
||||||
|
private $content;
|
||||||
|
|
||||||
|
public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->response = $response;
|
||||||
|
$this->content = &$content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatusCode(): int
|
||||||
|
{
|
||||||
|
return $this->response->getStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHeaders(bool $throw = true): array
|
||||||
|
{
|
||||||
|
return $this->response->getHeaders($throw);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContent(bool $throw = true): string
|
||||||
|
{
|
||||||
|
$this->content = $this->response->getContent(false);
|
||||||
|
|
||||||
|
if ($throw) {
|
||||||
|
$this->checkStatusCode($this->response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(bool $throw = true): array
|
||||||
|
{
|
||||||
|
$this->content = $this->response->toArray(false);
|
||||||
|
|
||||||
|
if ($throw) {
|
||||||
|
$this->checkStatusCode($this->response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function cancel(): void
|
||||||
|
{
|
||||||
|
$this->response->cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getInfo(string $type = null)
|
||||||
|
{
|
||||||
|
return $this->response->getInfo($type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Casts the response to a PHP stream resource.
|
||||||
|
*
|
||||||
|
* @return resource
|
||||||
|
*
|
||||||
|
* @throws TransportExceptionInterface When a network error occurs
|
||||||
|
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
|
||||||
|
* @throws ClientExceptionInterface On a 4xx when $throw is true
|
||||||
|
* @throws ServerExceptionInterface On a 5xx when $throw is true
|
||||||
|
*/
|
||||||
|
public function toStream(bool $throw = true)
|
||||||
|
{
|
||||||
|
if ($throw) {
|
||||||
|
// Ensure headers arrived
|
||||||
|
$this->response->getHeaders(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_callable([$this->response, 'toStream'])) {
|
||||||
|
return $this->response->toStream(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return StreamWrapper::createResource($this->response, $this->client);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkStatusCode($code)
|
||||||
|
{
|
||||||
|
if (500 <= $code) {
|
||||||
|
throw new ServerException($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (400 <= $code) {
|
||||||
|
throw new ClientException($this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (300 <= $code) {
|
||||||
|
throw new RedirectionException($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -36,10 +36,10 @@ class TraceableHttpClientTest extends TestCase
|
|||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
->willReturn(MockResponse::fromRequest('GET', '/foo/bar', ['options1' => 'foo'], new MockResponse()))
|
->willReturn(MockResponse::fromRequest('GET', '/foo/bar', ['options1' => 'foo'], new MockResponse('hello')))
|
||||||
;
|
;
|
||||||
$sut = new TraceableHttpClient($httpClient);
|
$sut = new TraceableHttpClient($httpClient);
|
||||||
$sut->request('GET', '/foo/bar', ['options1' => 'foo']);
|
$sut->request('GET', '/foo/bar', ['options1' => 'foo'])->getContent();
|
||||||
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
|
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
|
||||||
$actualTracedRequest = $tracedRequests[0];
|
$actualTracedRequest = $tracedRequests[0];
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
@ -47,6 +47,7 @@ class TraceableHttpClientTest extends TestCase
|
|||||||
'url' => '/foo/bar',
|
'url' => '/foo/bar',
|
||||||
'options' => ['options1' => 'foo'],
|
'options' => ['options1' => 'foo'],
|
||||||
'info' => [],
|
'info' => [],
|
||||||
|
'content' => 'hello',
|
||||||
], $actualTracedRequest);
|
], $actualTracedRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\HttpClient;
|
|||||||
|
|
||||||
use Psr\Log\LoggerAwareInterface;
|
use Psr\Log\LoggerAwareInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\HttpClient\Response\TraceableResponse;
|
||||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
|
||||||
@ -36,12 +37,14 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface,
|
|||||||
*/
|
*/
|
||||||
public function request(string $method, string $url, array $options = []): ResponseInterface
|
public function request(string $method, string $url, array $options = []): ResponseInterface
|
||||||
{
|
{
|
||||||
|
$content = '';
|
||||||
$traceInfo = [];
|
$traceInfo = [];
|
||||||
$this->tracedRequests[] = [
|
$this->tracedRequests[] = [
|
||||||
'method' => $method,
|
'method' => $method,
|
||||||
'url' => $url,
|
'url' => $url,
|
||||||
'options' => $options,
|
'options' => $options,
|
||||||
'info' => &$traceInfo,
|
'info' => &$traceInfo,
|
||||||
|
'content' => &$content,
|
||||||
];
|
];
|
||||||
$onProgress = $options['on_progress'] ?? null;
|
$onProgress = $options['on_progress'] ?? null;
|
||||||
|
|
||||||
@ -53,7 +56,7 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface,
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return $this->client->request($method, $url, $options);
|
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -61,7 +64,21 @@ final class TraceableHttpClient implements HttpClientInterface, ResetInterface,
|
|||||||
*/
|
*/
|
||||||
public function stream($responses, float $timeout = null): ResponseStreamInterface
|
public function stream($responses, float $timeout = null): ResponseStreamInterface
|
||||||
{
|
{
|
||||||
return $this->client->stream($responses, $timeout);
|
if ($responses instanceof TraceableResponse) {
|
||||||
|
$responses = [$responses];
|
||||||
|
} elseif (!is_iterable($responses)) {
|
||||||
|
throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of TraceableResponse objects, %s given.', __METHOD__, \is_object($responses) ? \get_class($responses) : \gettype($responses)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->client->stream(\Closure::bind(static function () use ($responses) {
|
||||||
|
foreach ($responses as $k => $r) {
|
||||||
|
if (!$r instanceof TraceableResponse) {
|
||||||
|
throw new \TypeError(sprintf('%s() expects parameter 1 to be an iterable of TraceableResponse objects, %s given.', __METHOD__, \is_object($r) ? \get_class($r) : \gettype($r)));
|
||||||
|
}
|
||||||
|
|
||||||
|
yield $k => $r->response;
|
||||||
|
}
|
||||||
|
}, null, TraceableResponse::class), $timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTracedRequests(): array
|
public function getTracedRequests(): array
|
||||||
|
@ -16,7 +16,7 @@ namespace Symfony\Component\VarDumper\Caster;
|
|||||||
*/
|
*/
|
||||||
class ImgStub extends ConstStub
|
class ImgStub extends ConstStub
|
||||||
{
|
{
|
||||||
public function __construct(string $data, string $contentType, string $size)
|
public function __construct(string $data, string $contentType, string $size = '')
|
||||||
{
|
{
|
||||||
$this->value = '';
|
$this->value = '';
|
||||||
$this->attr['img-data'] = $data;
|
$this->attr['img-data'] = $data;
|
||||||
|
@ -195,7 +195,7 @@ class CliDumper extends AbstractDumper
|
|||||||
'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0,
|
'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0,
|
||||||
'binary' => $bin,
|
'binary' => $bin,
|
||||||
];
|
];
|
||||||
$str = explode("\n", $str);
|
$str = $bin && false !== strpos($str, "\0") ? [$str] : explode("\n", $str);
|
||||||
if (isset($str[1]) && !isset($str[2]) && !isset($str[1][0])) {
|
if (isset($str[1]) && !isset($str[2]) && !isset($str[1][0])) {
|
||||||
unset($str[1]);
|
unset($str[1]);
|
||||||
$str[0] .= "\n";
|
$str[0] .= "\n";
|
||||||
|
@ -62,7 +62,7 @@ array:24 [
|
|||||||
6 => {$intMax}
|
6 => {$intMax}
|
||||||
"str" => "déjà\\n"
|
"str" => "déjà\\n"
|
||||||
7 => b"""
|
7 => b"""
|
||||||
é\\x00test\\t\\n
|
é\\x01test\\t\\n
|
||||||
ing
|
ing
|
||||||
"""
|
"""
|
||||||
"[]" => []
|
"[]" => []
|
||||||
|
@ -66,7 +66,7 @@ class HtmlDumperTest extends TestCase
|
|||||||
<span class=sf-dump-key>6</span> => <span class=sf-dump-num>{$intMax}</span>
|
<span class=sf-dump-key>6</span> => <span class=sf-dump-num>{$intMax}</span>
|
||||||
"<span class=sf-dump-key>str</span>" => "<span class=sf-dump-str title="5 characters">d&%s;j&%s;<span class="sf-dump-default sf-dump-ns">\\n</span></span>"
|
"<span class=sf-dump-key>str</span>" => "<span class=sf-dump-str title="5 characters">d&%s;j&%s;<span class="sf-dump-default sf-dump-ns">\\n</span></span>"
|
||||||
<span class=sf-dump-key>7</span> => b"""
|
<span class=sf-dump-key>7</span> => b"""
|
||||||
<span class=sf-dump-str title="11 binary or non-UTF-8 characters">é<span class="sf-dump-default">\\x00</span>test<span class="sf-dump-default">\\t</span><span class="sf-dump-default sf-dump-ns">\\n</span></span>
|
<span class=sf-dump-str title="11 binary or non-UTF-8 characters">é<span class="sf-dump-default">\\x01</span>test<span class="sf-dump-default">\\t</span><span class="sf-dump-default sf-dump-ns">\\n</span></span>
|
||||||
<span class=sf-dump-str title="11 binary or non-UTF-8 characters">ing</span>
|
<span class=sf-dump-str title="11 binary or non-UTF-8 characters">ing</span>
|
||||||
"""
|
"""
|
||||||
"<span class=sf-dump-key>[]</span>" => []
|
"<span class=sf-dump-key>[]</span>" => []
|
||||||
|
@ -17,7 +17,7 @@ $g = fopen(__FILE__, 'r');
|
|||||||
$var = [
|
$var = [
|
||||||
'number' => 1, null,
|
'number' => 1, null,
|
||||||
'const' => 1.1, true, false, NAN, INF, -INF, PHP_INT_MAX,
|
'const' => 1.1, true, false, NAN, INF, -INF, PHP_INT_MAX,
|
||||||
'str' => "déjà\n", "\xE9\x00test\t\ning",
|
'str' => "déjà\n", "\xE9\x01test\t\ning",
|
||||||
'[]' => [],
|
'[]' => [],
|
||||||
'res' => $g,
|
'res' => $g,
|
||||||
'obj' => $foo,
|
'obj' => $foo,
|
||||||
|
Reference in New Issue
Block a user