Skip to content

Commit

Permalink
feature #35407 [HttpClient] collect the body of responses when possib…
Browse files Browse the repository at this point in the history
…le (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
-------

121f728 [HttpClient] collect the body of responses when possible
  • Loading branch information
fabpot committed Jan 22, 2020
2 parents c4d15bc + 121f728 commit 07818f2
Show file tree
Hide file tree
Showing 9 changed files with 172 additions and 10 deletions.
Expand Up @@ -16,6 +16,7 @@
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>
Expand Down Expand Up @@ -128,8 +129,29 @@ private function collectOnClient(TraceableHttpClient $client): array
}
}

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']);
Expand Down
122 changes: 122 additions & 0 deletions src/Symfony/Component/HttpClient/Response/TraceableResponse.php
@@ -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);
}
}
}
Expand Up @@ -36,17 +36,18 @@ public function testItTracesRequest()
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([
'method' => 'GET',
'url' => '/foo/bar',
'options' => ['options1' => 'foo'],
'info' => [],
'content' => 'hello',
], $actualTracedRequest);
}

Expand Down
21 changes: 19 additions & 2 deletions src/Symfony/Component/HttpClient/TraceableHttpClient.php
Expand Up @@ -13,6 +13,7 @@

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;
Expand All @@ -36,12 +37,14 @@ public function __construct(HttpClientInterface $client)
*/
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;

Expand All @@ -53,15 +56,29 @@ public function request(string $method, string $url, array $options = []): Respo
}
};

return $this->client->request($method, $url, $options);
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content);
}

/**
* {@inheritdoc}
*/
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
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/VarDumper/Caster/ImgStub.php
Expand Up @@ -16,7 +16,7 @@
*/
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;
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Component/VarDumper/Dumper/CliDumper.php
Expand Up @@ -195,7 +195,7 @@ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut)
'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";
Expand Down
Expand Up @@ -62,7 +62,7 @@ public function testGet()
6 => {$intMax}
"str" => "déjà\\n"
7 => b"""
é\\x00test\\t\\n
é\\x01test\\t\\n
ing
"""
"[]" => []
Expand Down
Expand Up @@ -66,7 +66,7 @@ public function testGet()
<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">&eacute;<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">&eacute;<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>" => []
Expand Down
Expand Up @@ -17,7 +17,7 @@ class DumbFoo
$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,
Expand Down

0 comments on commit 07818f2

Please sign in to comment.