Skip to content

Commit e82c647

Browse files
[HttpClient] collect the body of responses when possible
1 parent 09727c2 commit e82c647

File tree

4 files changed

+167
-5
lines changed

4 files changed

+167
-5
lines changed

DataCollector/HttpClientDataCollector.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\HttpFoundation\Response;
1717
use Symfony\Component\HttpKernel\DataCollector\DataCollector;
1818
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
19+
use Symfony\Component\VarDumper\Caster\ImgStub;
1920

2021
/**
2122
* @author Jérémy Romey <[email protected]>
@@ -128,8 +129,29 @@ private function collectOnClient(TraceableHttpClient $client): array
128129
}
129130
}
130131

132+
if (\is_string($content = $trace['content'])) {
133+
$contentType = 'application/octet-stream';
134+
135+
foreach ($info['response_headers'] ?? [] as $h) {
136+
if (0 === stripos($h, 'content-type: ')) {
137+
$contentType = substr($h, \strlen('content-type: '));
138+
break;
139+
}
140+
}
141+
142+
if (0 === strpos($contentType, 'image/') && class_exists(ImgStub::class)) {
143+
$content = new ImgStub($content, $contentType, '');
144+
} else {
145+
$content = [$content];
146+
}
147+
148+
$k = 'response_content';
149+
} else {
150+
$k = 'response_json';
151+
}
152+
131153
$debugInfo = array_diff_key($info, $baseInfo);
132-
$info = array_diff_key($info, $debugInfo) + ['debug_info' => $debugInfo];
154+
$info = ['info' => $debugInfo] + array_diff_key($info, $debugInfo) + [$k => $content];
133155
unset($traces[$i]['info']); // break PHP reference used by TraceableHttpClient
134156
$traces[$i]['info'] = $this->cloneVar($info);
135157
$traces[$i]['options'] = $this->cloneVar($trace['options']);

Response/TraceableResponse.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpClient\Response;
13+
14+
use Symfony\Component\HttpClient\Exception\ClientException;
15+
use Symfony\Component\HttpClient\Exception\RedirectionException;
16+
use Symfony\Component\HttpClient\Exception\ServerException;
17+
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
18+
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
19+
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
20+
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
21+
use Symfony\Contracts\HttpClient\HttpClientInterface;
22+
use Symfony\Contracts\HttpClient\ResponseInterface;
23+
24+
/**
25+
* @author Nicolas Grekas <[email protected]>
26+
*
27+
* @internal
28+
*/
29+
class TraceableResponse implements ResponseInterface
30+
{
31+
private $client;
32+
private $response;
33+
private $content;
34+
35+
public function __construct(HttpClientInterface $client, ResponseInterface $response, &$content)
36+
{
37+
$this->client = $client;
38+
$this->response = $response;
39+
$this->content = &$content;
40+
}
41+
42+
public function getStatusCode(): int
43+
{
44+
return $this->response->getStatusCode();
45+
}
46+
47+
public function getHeaders(bool $throw = true): array
48+
{
49+
return $this->response->getHeaders($throw);
50+
}
51+
52+
public function getContent(bool $throw = true): string
53+
{
54+
$this->content = $this->response->getContent(false);
55+
56+
if ($throw) {
57+
$this->checkStatusCode($this->response->getStatusCode());
58+
}
59+
60+
return $this->content;
61+
}
62+
63+
public function toArray(bool $throw = true): array
64+
{
65+
$this->content = $this->response->toArray(false);
66+
67+
if ($throw) {
68+
$this->checkStatusCode($this->response->getStatusCode());
69+
}
70+
71+
return $this->content;
72+
}
73+
74+
public function cancel(): void
75+
{
76+
$this->response->cancel();
77+
}
78+
79+
public function getInfo(string $type = null)
80+
{
81+
return $this->response->getInfo($type);
82+
}
83+
84+
/**
85+
* Casts the response to a PHP stream resource.
86+
*
87+
* @return resource
88+
*
89+
* @throws TransportExceptionInterface When a network error occurs
90+
* @throws RedirectionExceptionInterface On a 3xx when $throw is true and the "max_redirects" option has been reached
91+
* @throws ClientExceptionInterface On a 4xx when $throw is true
92+
* @throws ServerExceptionInterface On a 5xx when $throw is true
93+
*/
94+
public function toStream(bool $throw = true)
95+
{
96+
if ($throw) {
97+
// Ensure headers arrived
98+
$this->response->getHeaders(true);
99+
}
100+
101+
if (\is_callable([$this->response, 'toStream'])) {
102+
return $this->response->toStream(false);
103+
}
104+
105+
return StreamWrapper::createResource($this->response, $this->client);
106+
}
107+
108+
private function checkStatusCode($code)
109+
{
110+
if (500 <= $code) {
111+
throw new ServerException($this);
112+
}
113+
114+
if (400 <= $code) {
115+
throw new ClientException($this);
116+
}
117+
118+
if (300 <= $code) {
119+
throw new RedirectionException($this);
120+
}
121+
}
122+
}

Tests/TraceableHttpClientTest.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,18 @@ public function testItTracesRequest()
3636
return true;
3737
})
3838
)
39-
->willReturn(MockResponse::fromRequest('GET', '/foo/bar', ['options1' => 'foo'], new MockResponse()))
39+
->willReturn(MockResponse::fromRequest('GET', '/foo/bar', ['options1' => 'foo'], new MockResponse('hello')))
4040
;
4141
$sut = new TraceableHttpClient($httpClient);
42-
$sut->request('GET', '/foo/bar', ['options1' => 'foo']);
42+
$sut->request('GET', '/foo/bar', ['options1' => 'foo'])->getContent();
4343
$this->assertCount(1, $tracedRequests = $sut->getTracedRequests());
4444
$actualTracedRequest = $tracedRequests[0];
4545
$this->assertEquals([
4646
'method' => 'GET',
4747
'url' => '/foo/bar',
4848
'options' => ['options1' => 'foo'],
4949
'info' => [],
50+
'content' => 'hello',
5051
], $actualTracedRequest);
5152
}
5253

TraceableHttpClient.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Psr\Log\LoggerAwareInterface;
1515
use Psr\Log\LoggerInterface;
16+
use Symfony\Component\HttpClient\Response\TraceableResponse;
1617
use Symfony\Contracts\HttpClient\HttpClientInterface;
1718
use Symfony\Contracts\HttpClient\ResponseInterface;
1819
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
@@ -36,12 +37,14 @@ public function __construct(HttpClientInterface $client)
3637
*/
3738
public function request(string $method, string $url, array $options = []): ResponseInterface
3839
{
40+
$content = '';
3941
$traceInfo = [];
4042
$this->tracedRequests[] = [
4143
'method' => $method,
4244
'url' => $url,
4345
'options' => $options,
4446
'info' => &$traceInfo,
47+
'content' => &$content,
4548
];
4649
$onProgress = $options['on_progress'] ?? null;
4750

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

56-
return $this->client->request($method, $url, $options);
59+
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $content);
5760
}
5861

5962
/**
6063
* {@inheritdoc}
6164
*/
6265
public function stream($responses, float $timeout = null): ResponseStreamInterface
6366
{
64-
return $this->client->stream($responses, $timeout);
67+
if ($responses instanceof TraceableResponse) {
68+
$responses = [$responses];
69+
} elseif (!is_iterable($responses)) {
70+
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)));
71+
}
72+
73+
return $this->client->stream(\Closure::bind(static function () use ($responses) {
74+
foreach ($responses as $k => $r) {
75+
if (!$r instanceof TraceableResponse) {
76+
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)));
77+
}
78+
79+
yield $k => $r->response;
80+
}
81+
}, null, TraceableResponse::class), $timeout);
6582
}
6683

6784
public function getTracedRequests(): array

0 commit comments

Comments
 (0)