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\HttpKernel\DataCollector\DataCollector;
|
||||
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
|
||||
use Symfony\Component\VarDumper\Caster\ImgStub;
|
||||
|
||||
/**
|
||||
* @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);
|
||||
$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
|
||||
$traces[$i]['info'] = $this->cloneVar($info);
|
||||
$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;
|
||||
})
|
||||
)
|
||||
->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->request('GET', '/foo/bar', ['options1' => 'foo']);
|
||||
$sut->request('GET', '/foo/bar', ['options1' => 'foo'])->getContent();
|
||||
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
|
||||
$actualTracedRequest = $tracedRequests[0];
|
||||
$this->assertEquals([
|
||||
@ -47,6 +47,7 @@ class TraceableHttpClientTest extends TestCase
|
||||
'url' => '/foo/bar',
|
||||
'options' => ['options1' => 'foo'],
|
||||
'info' => [],
|
||||
'content' => 'hello',
|
||||
], $actualTracedRequest);
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\HttpClient;
|
||||
|
||||
use Psr\Log\LoggerAwareInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Response\TraceableResponse;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
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
|
||||
{
|
||||
$content = '';
|
||||
$traceInfo = [];
|
||||
$this->tracedRequests[] = [
|
||||
'method' => $method,
|
||||
'url' => $url,
|
||||
'options' => $options,
|
||||
'info' => &$traceInfo,
|
||||
'content' => &$content,
|
||||
];
|
||||
$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
|
||||
{
|
||||
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
|
||||
|
@ -16,7 +16,7 @@ namespace Symfony\Component\VarDumper\Caster;
|
||||
*/
|
||||
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->attr['img-data'] = $data;
|
||||
|
@ -195,7 +195,7 @@ class CliDumper extends AbstractDumper
|
||||
'length' => 0 <= $cut ? mb_strlen($str, 'UTF-8') + $cut : 0,
|
||||
'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])) {
|
||||
unset($str[1]);
|
||||
$str[0] .= "\n";
|
||||
|
@ -62,7 +62,7 @@ array:24 [
|
||||
6 => {$intMax}
|
||||
"str" => "déjà\\n"
|
||||
7 => b"""
|
||||
é\\x00test\\t\\n
|
||||
é\\x01test\\t\\n
|
||||
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>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-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-key>[]</span>" => []
|
||||
|
@ -17,7 +17,7 @@ $g = fopen(__FILE__, 'r');
|
||||
$var = [
|
||||
'number' => 1, null,
|
||||
'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,
|
||||
'obj' => $foo,
|
||||
|
Reference in New Issue
Block a user