HttpClientTestCase.php 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Contracts\HttpClient\Test;
  11. use PHPUnit\Framework\TestCase;
  12. use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
  13. use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
  14. use Symfony\Contracts\HttpClient\Exception\TimeoutExceptionInterface;
  15. use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
  16. use Symfony\Contracts\HttpClient\HttpClientInterface;
  17. /**
  18. * A reference test suite for HttpClientInterface implementations.
  19. */
  20. abstract class HttpClientTestCase extends TestCase
  21. {
  22. public static function setUpBeforeClass(): void
  23. {
  24. TestHttpServer::start();
  25. }
  26. public static function tearDownAfterClass(): void
  27. {
  28. TestHttpServer::stop(8067);
  29. TestHttpServer::stop(8077);
  30. }
  31. abstract protected function getHttpClient(string $testCase): HttpClientInterface;
  32. public function testGetRequest()
  33. {
  34. $client = $this->getHttpClient(__FUNCTION__);
  35. $response = $client->request('GET', 'http://localhost:8057', [
  36. 'headers' => ['Foo' => 'baR'],
  37. 'user_data' => $data = new \stdClass(),
  38. ]);
  39. $this->assertSame([], $response->getInfo('response_headers'));
  40. $this->assertSame($data, $response->getInfo()['user_data']);
  41. $this->assertSame(200, $response->getStatusCode());
  42. $info = $response->getInfo();
  43. $this->assertNull($info['error']);
  44. $this->assertSame(0, $info['redirect_count']);
  45. $this->assertSame('HTTP/1.1 200 OK', $info['response_headers'][0]);
  46. $this->assertSame('Host: localhost:8057', $info['response_headers'][1]);
  47. $this->assertSame('http://localhost:8057/', $info['url']);
  48. $headers = $response->getHeaders();
  49. $this->assertSame('localhost:8057', $headers['host'][0]);
  50. $this->assertSame(['application/json'], $headers['content-type']);
  51. $body = json_decode($response->getContent(), true);
  52. $this->assertSame($body, $response->toArray());
  53. $this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
  54. $this->assertSame('/', $body['REQUEST_URI']);
  55. $this->assertSame('GET', $body['REQUEST_METHOD']);
  56. $this->assertSame('localhost:8057', $body['HTTP_HOST']);
  57. $this->assertSame('baR', $body['HTTP_FOO']);
  58. $response = $client->request('GET', 'http://localhost:8057/length-broken');
  59. $this->expectException(TransportExceptionInterface::class);
  60. $response->getContent();
  61. }
  62. public function testHeadRequest()
  63. {
  64. $client = $this->getHttpClient(__FUNCTION__);
  65. $response = $client->request('HEAD', 'http://localhost:8057/head', [
  66. 'headers' => ['Foo' => 'baR'],
  67. 'user_data' => $data = new \stdClass(),
  68. 'buffer' => false,
  69. ]);
  70. $this->assertSame([], $response->getInfo('response_headers'));
  71. $this->assertSame(200, $response->getStatusCode());
  72. $info = $response->getInfo();
  73. $this->assertSame('HTTP/1.1 200 OK', $info['response_headers'][0]);
  74. $this->assertSame('Host: localhost:8057', $info['response_headers'][1]);
  75. $headers = $response->getHeaders();
  76. $this->assertSame('localhost:8057', $headers['host'][0]);
  77. $this->assertSame(['application/json'], $headers['content-type']);
  78. $this->assertTrue(0 < $headers['content-length'][0]);
  79. $this->assertSame('', $response->getContent());
  80. }
  81. public function testNonBufferedGetRequest()
  82. {
  83. $client = $this->getHttpClient(__FUNCTION__);
  84. $response = $client->request('GET', 'http://localhost:8057', [
  85. 'buffer' => false,
  86. 'headers' => ['Foo' => 'baR'],
  87. ]);
  88. $body = $response->toArray();
  89. $this->assertSame('baR', $body['HTTP_FOO']);
  90. $this->expectException(TransportExceptionInterface::class);
  91. $response->getContent();
  92. }
  93. public function testBufferSink()
  94. {
  95. $sink = fopen('php://temp', 'w+');
  96. $client = $this->getHttpClient(__FUNCTION__);
  97. $response = $client->request('GET', 'http://localhost:8057', [
  98. 'buffer' => $sink,
  99. 'headers' => ['Foo' => 'baR'],
  100. ]);
  101. $body = $response->toArray();
  102. $this->assertSame('baR', $body['HTTP_FOO']);
  103. rewind($sink);
  104. $sink = stream_get_contents($sink);
  105. $this->assertSame($sink, $response->getContent());
  106. }
  107. public function testConditionalBuffering()
  108. {
  109. $client = $this->getHttpClient(__FUNCTION__);
  110. $response = $client->request('GET', 'http://localhost:8057');
  111. $firstContent = $response->getContent();
  112. $secondContent = $response->getContent();
  113. $this->assertSame($firstContent, $secondContent);
  114. $response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () { return false; }]);
  115. $response->getContent();
  116. $this->expectException(TransportExceptionInterface::class);
  117. $response->getContent();
  118. }
  119. public function testReentrantBufferCallback()
  120. {
  121. $client = $this->getHttpClient(__FUNCTION__);
  122. $response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () use (&$response) {
  123. $response->cancel();
  124. return true;
  125. }]);
  126. $this->assertSame(200, $response->getStatusCode());
  127. $this->expectException(TransportExceptionInterface::class);
  128. $response->getContent();
  129. }
  130. public function testThrowingBufferCallback()
  131. {
  132. $client = $this->getHttpClient(__FUNCTION__);
  133. $response = $client->request('GET', 'http://localhost:8057', ['buffer' => function () {
  134. throw new \Exception('Boo.');
  135. }]);
  136. $this->assertSame(200, $response->getStatusCode());
  137. $this->expectException(TransportExceptionInterface::class);
  138. $this->expectExceptionMessage('Boo');
  139. $response->getContent();
  140. }
  141. public function testUnsupportedOption()
  142. {
  143. $client = $this->getHttpClient(__FUNCTION__);
  144. $this->expectException(\InvalidArgumentException::class);
  145. $client->request('GET', 'http://localhost:8057', [
  146. 'capture_peer_cert' => 1.0,
  147. ]);
  148. }
  149. public function testHttpVersion()
  150. {
  151. $client = $this->getHttpClient(__FUNCTION__);
  152. $response = $client->request('GET', 'http://localhost:8057', [
  153. 'http_version' => 1.0,
  154. ]);
  155. $this->assertSame(200, $response->getStatusCode());
  156. $this->assertSame('HTTP/1.0 200 OK', $response->getInfo('response_headers')[0]);
  157. $body = $response->toArray();
  158. $this->assertSame('HTTP/1.0', $body['SERVER_PROTOCOL']);
  159. $this->assertSame('GET', $body['REQUEST_METHOD']);
  160. $this->assertSame('/', $body['REQUEST_URI']);
  161. }
  162. public function testChunkedEncoding()
  163. {
  164. $client = $this->getHttpClient(__FUNCTION__);
  165. $response = $client->request('GET', 'http://localhost:8057/chunked');
  166. $this->assertSame(['chunked'], $response->getHeaders()['transfer-encoding']);
  167. $this->assertSame('Symfony is awesome!', $response->getContent());
  168. $response = $client->request('GET', 'http://localhost:8057/chunked-broken');
  169. $this->expectException(TransportExceptionInterface::class);
  170. $response->getContent();
  171. }
  172. public function testClientError()
  173. {
  174. $client = $this->getHttpClient(__FUNCTION__);
  175. $response = $client->request('GET', 'http://localhost:8057/404');
  176. $client->stream($response)->valid();
  177. $this->assertSame(404, $response->getInfo('http_code'));
  178. try {
  179. $response->getHeaders();
  180. $this->fail(ClientExceptionInterface::class.' expected');
  181. } catch (ClientExceptionInterface $e) {
  182. }
  183. try {
  184. $response->getContent();
  185. $this->fail(ClientExceptionInterface::class.' expected');
  186. } catch (ClientExceptionInterface $e) {
  187. }
  188. $this->assertSame(404, $response->getStatusCode());
  189. $this->assertSame(['application/json'], $response->getHeaders(false)['content-type']);
  190. $this->assertNotEmpty($response->getContent(false));
  191. $response = $client->request('GET', 'http://localhost:8057/404');
  192. try {
  193. foreach ($client->stream($response) as $chunk) {
  194. $this->assertTrue($chunk->isFirst());
  195. }
  196. $this->fail(ClientExceptionInterface::class.' expected');
  197. } catch (ClientExceptionInterface $e) {
  198. }
  199. }
  200. public function testIgnoreErrors()
  201. {
  202. $client = $this->getHttpClient(__FUNCTION__);
  203. $response = $client->request('GET', 'http://localhost:8057/404');
  204. $this->assertSame(404, $response->getStatusCode());
  205. }
  206. public function testDnsError()
  207. {
  208. $client = $this->getHttpClient(__FUNCTION__);
  209. $response = $client->request('GET', 'http://localhost:8057/301/bad-tld');
  210. try {
  211. $response->getStatusCode();
  212. $this->fail(TransportExceptionInterface::class.' expected');
  213. } catch (TransportExceptionInterface $e) {
  214. $this->addToAssertionCount(1);
  215. }
  216. try {
  217. $response->getStatusCode();
  218. $this->fail(TransportExceptionInterface::class.' still expected');
  219. } catch (TransportExceptionInterface $e) {
  220. $this->addToAssertionCount(1);
  221. }
  222. $response = $client->request('GET', 'http://localhost:8057/301/bad-tld');
  223. try {
  224. foreach ($client->stream($response) as $r => $chunk) {
  225. }
  226. $this->fail(TransportExceptionInterface::class.' expected');
  227. } catch (TransportExceptionInterface $e) {
  228. $this->addToAssertionCount(1);
  229. }
  230. $this->assertSame($response, $r);
  231. $this->assertNotNull($chunk->getError());
  232. $this->expectException(TransportExceptionInterface::class);
  233. foreach ($client->stream($response) as $chunk) {
  234. }
  235. }
  236. public function testInlineAuth()
  237. {
  238. $client = $this->getHttpClient(__FUNCTION__);
  239. $response = $client->request('GET', 'http://foo:bar%3Dbar@localhost:8057');
  240. $body = $response->toArray();
  241. $this->assertSame('foo', $body['PHP_AUTH_USER']);
  242. $this->assertSame('bar=bar', $body['PHP_AUTH_PW']);
  243. }
  244. public function testBadRequestBody()
  245. {
  246. $client = $this->getHttpClient(__FUNCTION__);
  247. $this->expectException(TransportExceptionInterface::class);
  248. $response = $client->request('POST', 'http://localhost:8057/', [
  249. 'body' => function () { yield []; },
  250. ]);
  251. $response->getStatusCode();
  252. }
  253. public function test304()
  254. {
  255. $client = $this->getHttpClient(__FUNCTION__);
  256. $response = $client->request('GET', 'http://localhost:8057/304', [
  257. 'headers' => ['If-Match' => '"abc"'],
  258. 'buffer' => false,
  259. ]);
  260. $this->assertSame(304, $response->getStatusCode());
  261. $this->assertSame('', $response->getContent(false));
  262. }
  263. /**
  264. * @testWith [[]]
  265. * [["Content-Length: 7"]]
  266. */
  267. public function testRedirects(array $headers = [])
  268. {
  269. $client = $this->getHttpClient(__FUNCTION__);
  270. $response = $client->request('POST', 'http://localhost:8057/301', [
  271. 'auth_basic' => 'foo:bar',
  272. 'headers' => $headers,
  273. 'body' => function () {
  274. yield 'foo=bar';
  275. },
  276. ]);
  277. $body = $response->toArray();
  278. $this->assertSame('GET', $body['REQUEST_METHOD']);
  279. $this->assertSame('Basic Zm9vOmJhcg==', $body['HTTP_AUTHORIZATION']);
  280. $this->assertSame('http://localhost:8057/', $response->getInfo('url'));
  281. $this->assertSame(2, $response->getInfo('redirect_count'));
  282. $this->assertNull($response->getInfo('redirect_url'));
  283. $expected = [
  284. 'HTTP/1.1 301 Moved Permanently',
  285. 'Location: http://127.0.0.1:8057/302',
  286. 'Content-Type: application/json',
  287. 'HTTP/1.1 302 Found',
  288. 'Location: http://localhost:8057/',
  289. 'Content-Type: application/json',
  290. 'HTTP/1.1 200 OK',
  291. 'Content-Type: application/json',
  292. ];
  293. $filteredHeaders = array_values(array_filter($response->getInfo('response_headers'), function ($h) {
  294. return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true) && 'Content-Encoding: gzip' !== $h;
  295. }));
  296. $this->assertSame($expected, $filteredHeaders);
  297. }
  298. public function testInvalidRedirect()
  299. {
  300. $client = $this->getHttpClient(__FUNCTION__);
  301. $response = $client->request('GET', 'http://localhost:8057/301/invalid');
  302. $this->assertSame(301, $response->getStatusCode());
  303. $this->assertSame(['//?foo=bar'], $response->getHeaders(false)['location']);
  304. $this->assertSame(0, $response->getInfo('redirect_count'));
  305. $this->assertNull($response->getInfo('redirect_url'));
  306. $this->expectException(RedirectionExceptionInterface::class);
  307. $response->getHeaders();
  308. }
  309. public function testRelativeRedirects()
  310. {
  311. $client = $this->getHttpClient(__FUNCTION__);
  312. $response = $client->request('GET', 'http://localhost:8057/302/relative');
  313. $body = $response->toArray();
  314. $this->assertSame('/', $body['REQUEST_URI']);
  315. $this->assertNull($response->getInfo('redirect_url'));
  316. $response = $client->request('GET', 'http://localhost:8057/302/relative', [
  317. 'max_redirects' => 0,
  318. ]);
  319. $this->assertSame(302, $response->getStatusCode());
  320. $this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));
  321. }
  322. public function testRedirect307()
  323. {
  324. $client = $this->getHttpClient(__FUNCTION__);
  325. $response = $client->request('POST', 'http://localhost:8057/307', [
  326. 'body' => function () {
  327. yield 'foo=bar';
  328. },
  329. 'max_redirects' => 0,
  330. ]);
  331. $this->assertSame(307, $response->getStatusCode());
  332. $response = $client->request('POST', 'http://localhost:8057/307', [
  333. 'body' => 'foo=bar',
  334. ]);
  335. $body = $response->toArray();
  336. $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
  337. }
  338. public function testMaxRedirects()
  339. {
  340. $client = $this->getHttpClient(__FUNCTION__);
  341. $response = $client->request('GET', 'http://localhost:8057/301', [
  342. 'max_redirects' => 1,
  343. 'auth_basic' => 'foo:bar',
  344. ]);
  345. try {
  346. $response->getHeaders();
  347. $this->fail(RedirectionExceptionInterface::class.' expected');
  348. } catch (RedirectionExceptionInterface $e) {
  349. }
  350. $this->assertSame(302, $response->getStatusCode());
  351. $this->assertSame(1, $response->getInfo('redirect_count'));
  352. $this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));
  353. $expected = [
  354. 'HTTP/1.1 301 Moved Permanently',
  355. 'Location: http://127.0.0.1:8057/302',
  356. 'Content-Type: application/json',
  357. 'HTTP/1.1 302 Found',
  358. 'Location: http://localhost:8057/',
  359. 'Content-Type: application/json',
  360. ];
  361. $filteredHeaders = array_values(array_filter($response->getInfo('response_headers'), function ($h) {
  362. return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true);
  363. }));
  364. $this->assertSame($expected, $filteredHeaders);
  365. }
  366. public function testStream()
  367. {
  368. $client = $this->getHttpClient(__FUNCTION__);
  369. $response = $client->request('GET', 'http://localhost:8057');
  370. $chunks = $client->stream($response);
  371. $result = [];
  372. foreach ($chunks as $r => $chunk) {
  373. if ($chunk->isTimeout()) {
  374. $result[] = 't';
  375. } elseif ($chunk->isLast()) {
  376. $result[] = 'l';
  377. } elseif ($chunk->isFirst()) {
  378. $result[] = 'f';
  379. }
  380. }
  381. $this->assertSame($response, $r);
  382. $this->assertSame(['f', 'l'], $result);
  383. $chunk = null;
  384. $i = 0;
  385. foreach ($client->stream($response) as $chunk) {
  386. ++$i;
  387. }
  388. $this->assertSame(1, $i);
  389. $this->assertTrue($chunk->isLast());
  390. }
  391. public function testAddToStream()
  392. {
  393. $client = $this->getHttpClient(__FUNCTION__);
  394. $r1 = $client->request('GET', 'http://localhost:8057');
  395. $completed = [];
  396. $pool = [$r1];
  397. while ($pool) {
  398. $chunks = $client->stream($pool);
  399. $pool = [];
  400. foreach ($chunks as $r => $chunk) {
  401. if (!$chunk->isLast()) {
  402. continue;
  403. }
  404. if ($r1 === $r) {
  405. $r2 = $client->request('GET', 'http://localhost:8057');
  406. $pool[] = $r2;
  407. }
  408. $completed[] = $r;
  409. }
  410. }
  411. $this->assertSame([$r1, $r2], $completed);
  412. }
  413. public function testCompleteTypeError()
  414. {
  415. $client = $this->getHttpClient(__FUNCTION__);
  416. $this->expectException(\TypeError::class);
  417. $client->stream(123);
  418. }
  419. public function testOnProgress()
  420. {
  421. $client = $this->getHttpClient(__FUNCTION__);
  422. $response = $client->request('POST', 'http://localhost:8057/post', [
  423. 'headers' => ['Content-Length' => 14],
  424. 'body' => 'foo=0123456789',
  425. 'on_progress' => function (...$state) use (&$steps) { $steps[] = $state; },
  426. ]);
  427. $body = $response->toArray();
  428. $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
  429. $this->assertSame([0, 0], \array_slice($steps[0], 0, 2));
  430. $lastStep = \array_slice($steps, -1)[0];
  431. $this->assertSame([57, 57], \array_slice($lastStep, 0, 2));
  432. $this->assertSame('http://localhost:8057/post', $steps[0][2]['url']);
  433. }
  434. public function testPostJson()
  435. {
  436. $client = $this->getHttpClient(__FUNCTION__);
  437. $response = $client->request('POST', 'http://localhost:8057/post', [
  438. 'json' => ['foo' => 'bar'],
  439. ]);
  440. $body = $response->toArray();
  441. $this->assertStringContainsString('json', $body['content-type']);
  442. unset($body['content-type']);
  443. $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
  444. }
  445. public function testPostArray()
  446. {
  447. $client = $this->getHttpClient(__FUNCTION__);
  448. $response = $client->request('POST', 'http://localhost:8057/post', [
  449. 'body' => ['foo' => 'bar'],
  450. ]);
  451. $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $response->toArray());
  452. }
  453. public function testPostResource()
  454. {
  455. $client = $this->getHttpClient(__FUNCTION__);
  456. $h = fopen('php://temp', 'w+');
  457. fwrite($h, 'foo=0123456789');
  458. rewind($h);
  459. $response = $client->request('POST', 'http://localhost:8057/post', [
  460. 'body' => $h,
  461. ]);
  462. $body = $response->toArray();
  463. $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
  464. }
  465. public function testPostCallback()
  466. {
  467. $client = $this->getHttpClient(__FUNCTION__);
  468. $response = $client->request('POST', 'http://localhost:8057/post', [
  469. 'body' => function () {
  470. yield 'foo';
  471. yield '';
  472. yield '=';
  473. yield '0123456789';
  474. },
  475. ]);
  476. $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $response->toArray());
  477. }
  478. public function testCancel()
  479. {
  480. $client = $this->getHttpClient(__FUNCTION__);
  481. $response = $client->request('GET', 'http://localhost:8057/timeout-header');
  482. $response->cancel();
  483. $this->expectException(TransportExceptionInterface::class);
  484. $response->getHeaders();
  485. }
  486. public function testInfoOnCanceledResponse()
  487. {
  488. $client = $this->getHttpClient(__FUNCTION__);
  489. $response = $client->request('GET', 'http://localhost:8057/timeout-header');
  490. $this->assertFalse($response->getInfo('canceled'));
  491. $response->cancel();
  492. $this->assertTrue($response->getInfo('canceled'));
  493. }
  494. public function testCancelInStream()
  495. {
  496. $client = $this->getHttpClient(__FUNCTION__);
  497. $response = $client->request('GET', 'http://localhost:8057/404');
  498. foreach ($client->stream($response) as $chunk) {
  499. $response->cancel();
  500. }
  501. $this->expectException(TransportExceptionInterface::class);
  502. foreach ($client->stream($response) as $chunk) {
  503. }
  504. }
  505. public function testOnProgressCancel()
  506. {
  507. $client = $this->getHttpClient(__FUNCTION__);
  508. $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
  509. 'on_progress' => function ($dlNow) {
  510. if (0 < $dlNow) {
  511. throw new \Exception('Aborting the request.');
  512. }
  513. },
  514. ]);
  515. try {
  516. foreach ($client->stream([$response]) as $chunk) {
  517. }
  518. $this->fail(ClientExceptionInterface::class.' expected');
  519. } catch (TransportExceptionInterface $e) {
  520. $this->assertSame('Aborting the request.', $e->getPrevious()->getMessage());
  521. }
  522. $this->assertNotNull($response->getInfo('error'));
  523. $this->expectException(TransportExceptionInterface::class);
  524. $response->getContent();
  525. }
  526. public function testOnProgressError()
  527. {
  528. $client = $this->getHttpClient(__FUNCTION__);
  529. $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
  530. 'on_progress' => function ($dlNow) {
  531. if (0 < $dlNow) {
  532. throw new \Error('BUG.');
  533. }
  534. },
  535. ]);
  536. try {
  537. foreach ($client->stream([$response]) as $chunk) {
  538. }
  539. $this->fail('Error expected');
  540. } catch (\Error $e) {
  541. $this->assertSame('BUG.', $e->getMessage());
  542. }
  543. $this->assertNotNull($response->getInfo('error'));
  544. $this->expectException(TransportExceptionInterface::class);
  545. $response->getContent();
  546. }
  547. public function testResolve()
  548. {
  549. $client = $this->getHttpClient(__FUNCTION__);
  550. $response = $client->request('GET', 'http://symfony.com:8057/', [
  551. 'resolve' => ['symfony.com' => '127.0.0.1'],
  552. ]);
  553. $this->assertSame(200, $response->getStatusCode());
  554. $this->assertSame(200, $client->request('GET', 'http://symfony.com:8057/')->getStatusCode());
  555. $response = null;
  556. $this->expectException(TransportExceptionInterface::class);
  557. $client->request('GET', 'http://symfony.com:8057/', ['timeout' => 1]);
  558. }
  559. public function testIdnResolve()
  560. {
  561. $client = $this->getHttpClient(__FUNCTION__);
  562. $response = $client->request('GET', 'http://0-------------------------------------------------------------0.com:8057/', [
  563. 'resolve' => ['0-------------------------------------------------------------0.com' => '127.0.0.1'],
  564. ]);
  565. $this->assertSame(200, $response->getStatusCode());
  566. $response = $client->request('GET', 'http://Bücher.example:8057/', [
  567. 'resolve' => ['xn--bcher-kva.example' => '127.0.0.1'],
  568. ]);
  569. $this->assertSame(200, $response->getStatusCode());
  570. }
  571. public function testNotATimeout()
  572. {
  573. $client = $this->getHttpClient(__FUNCTION__);
  574. $response = $client->request('GET', 'http://localhost:8057/timeout-header', [
  575. 'timeout' => 0.9,
  576. ]);
  577. sleep(1);
  578. $this->assertSame(200, $response->getStatusCode());
  579. }
  580. public function testTimeoutOnAccess()
  581. {
  582. $client = $this->getHttpClient(__FUNCTION__);
  583. $response = $client->request('GET', 'http://localhost:8057/timeout-header', [
  584. 'timeout' => 0.1,
  585. ]);
  586. $this->expectException(TransportExceptionInterface::class);
  587. $response->getHeaders();
  588. }
  589. public function testTimeoutIsNotAFatalError()
  590. {
  591. usleep(300000); // wait for the previous test to release the server
  592. $client = $this->getHttpClient(__FUNCTION__);
  593. $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
  594. 'timeout' => 0.25,
  595. ]);
  596. try {
  597. $response->getContent();
  598. $this->fail(TimeoutExceptionInterface::class.' expected');
  599. } catch (TimeoutExceptionInterface $e) {
  600. }
  601. for ($i = 0; $i < 10; ++$i) {
  602. try {
  603. $this->assertSame('<1><2>', $response->getContent());
  604. break;
  605. } catch (TimeoutExceptionInterface $e) {
  606. }
  607. }
  608. if (10 === $i) {
  609. throw $e;
  610. }
  611. }
  612. public function testTimeoutOnStream()
  613. {
  614. $client = $this->getHttpClient(__FUNCTION__);
  615. $response = $client->request('GET', 'http://localhost:8057/timeout-body');
  616. $this->assertSame(200, $response->getStatusCode());
  617. $chunks = $client->stream([$response], 0.2);
  618. $result = [];
  619. foreach ($chunks as $r => $chunk) {
  620. if ($chunk->isTimeout()) {
  621. $result[] = 't';
  622. } else {
  623. $result[] = $chunk->getContent();
  624. }
  625. }
  626. $this->assertSame(['<1>', 't'], $result);
  627. $chunks = $client->stream([$response]);
  628. foreach ($chunks as $r => $chunk) {
  629. $this->assertSame('<2>', $chunk->getContent());
  630. $this->assertSame('<1><2>', $r->getContent());
  631. return;
  632. }
  633. $this->fail('The response should have completed');
  634. }
  635. public function testUncheckedTimeoutThrows()
  636. {
  637. $client = $this->getHttpClient(__FUNCTION__);
  638. $response = $client->request('GET', 'http://localhost:8057/timeout-body');
  639. $chunks = $client->stream([$response], 0.1);
  640. $this->expectException(TransportExceptionInterface::class);
  641. foreach ($chunks as $r => $chunk) {
  642. }
  643. }
  644. public function testTimeoutWithActiveConcurrentStream()
  645. {
  646. $p1 = TestHttpServer::start(8067);
  647. $p2 = TestHttpServer::start(8077);
  648. $client = $this->getHttpClient(__FUNCTION__);
  649. $streamingResponse = $client->request('GET', 'http://localhost:8067/max-duration');
  650. $blockingResponse = $client->request('GET', 'http://localhost:8077/timeout-body', [
  651. 'timeout' => 0.25,
  652. ]);
  653. $this->assertSame(200, $streamingResponse->getStatusCode());
  654. $this->assertSame(200, $blockingResponse->getStatusCode());
  655. $this->expectException(TransportExceptionInterface::class);
  656. try {
  657. $blockingResponse->getContent();
  658. } finally {
  659. $p1->stop();
  660. $p2->stop();
  661. }
  662. }
  663. public function testTimeoutOnInitialize()
  664. {
  665. $p1 = TestHttpServer::start(8067);
  666. $p2 = TestHttpServer::start(8077);
  667. $client = $this->getHttpClient(__FUNCTION__);
  668. $start = microtime(true);
  669. $responses = [];
  670. $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
  671. $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
  672. $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
  673. $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
  674. try {
  675. foreach ($responses as $response) {
  676. try {
  677. $response->getContent();
  678. $this->fail(TransportExceptionInterface::class.' expected');
  679. } catch (TransportExceptionInterface $e) {
  680. }
  681. }
  682. $responses = [];
  683. $duration = microtime(true) - $start;
  684. $this->assertLessThan(1.0, $duration);
  685. } finally {
  686. $p1->stop();
  687. $p2->stop();
  688. }
  689. }
  690. public function testTimeoutOnDestruct()
  691. {
  692. $p1 = TestHttpServer::start(8067);
  693. $p2 = TestHttpServer::start(8077);
  694. $client = $this->getHttpClient(__FUNCTION__);
  695. $start = microtime(true);
  696. $responses = [];
  697. $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
  698. $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
  699. $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
  700. $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
  701. try {
  702. while ($response = array_shift($responses)) {
  703. try {
  704. unset($response);
  705. $this->fail(TransportExceptionInterface::class.' expected');
  706. } catch (TransportExceptionInterface $e) {
  707. }
  708. }
  709. $duration = microtime(true) - $start;
  710. $this->assertLessThan(1.0, $duration);
  711. } finally {
  712. $p1->stop();
  713. $p2->stop();
  714. }
  715. }
  716. public function testDestruct()
  717. {
  718. $client = $this->getHttpClient(__FUNCTION__);
  719. $start = microtime(true);
  720. $client->request('GET', 'http://localhost:8057/timeout-long');
  721. $client = null;
  722. $duration = microtime(true) - $start;
  723. $this->assertGreaterThan(1, $duration);
  724. $this->assertLessThan(4, $duration);
  725. }
  726. public function testGetContentAfterDestruct()
  727. {
  728. $client = $this->getHttpClient(__FUNCTION__);
  729. try {
  730. $client->request('GET', 'http://localhost:8057/404');
  731. $this->fail(ClientExceptionInterface::class.' expected');
  732. } catch (ClientExceptionInterface $e) {
  733. $this->assertSame('GET', $e->getResponse()->toArray(false)['REQUEST_METHOD']);
  734. }
  735. }
  736. public function testGetEncodedContentAfterDestruct()
  737. {
  738. $client = $this->getHttpClient(__FUNCTION__);
  739. try {
  740. $client->request('GET', 'http://localhost:8057/404-gzipped');
  741. $this->fail(ClientExceptionInterface::class.' expected');
  742. } catch (ClientExceptionInterface $e) {
  743. $this->assertSame('some text', $e->getResponse()->getContent(false));
  744. }
  745. }
  746. public function testProxy()
  747. {
  748. $client = $this->getHttpClient(__FUNCTION__);
  749. $response = $client->request('GET', 'http://localhost:8057/', [
  750. 'proxy' => 'http://localhost:8057',
  751. ]);
  752. $body = $response->toArray();
  753. $this->assertSame('localhost:8057', $body['HTTP_HOST']);
  754. $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
  755. $response = $client->request('GET', 'http://localhost:8057/', [
  756. 'proxy' => 'http://foo:b%3Dar@localhost:8057',
  757. ]);
  758. $body = $response->toArray();
  759. $this->assertSame('Basic Zm9vOmI9YXI=', $body['HTTP_PROXY_AUTHORIZATION']);
  760. $_SERVER['http_proxy'] = 'http://localhost:8057';
  761. try {
  762. $response = $client->request('GET', 'http://localhost:8057/');
  763. $body = $response->toArray();
  764. $this->assertSame('localhost:8057', $body['HTTP_HOST']);
  765. $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
  766. } finally {
  767. unset($_SERVER['http_proxy']);
  768. }
  769. $response = $client->request('GET', 'http://localhost:8057/301/proxy', [
  770. 'proxy' => 'http://localhost:8057',
  771. ]);
  772. $body = $response->toArray();
  773. $this->assertSame('localhost:8057', $body['HTTP_HOST']);
  774. $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
  775. }
  776. public function testNoProxy()
  777. {
  778. putenv('no_proxy='.$_SERVER['no_proxy'] = 'example.com, localhost');
  779. try {
  780. $client = $this->getHttpClient(__FUNCTION__);
  781. $response = $client->request('GET', 'http://localhost:8057/', [
  782. 'proxy' => 'http://localhost:8057',
  783. ]);
  784. $body = $response->toArray();
  785. $this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
  786. $this->assertSame('/', $body['REQUEST_URI']);
  787. $this->assertSame('GET', $body['REQUEST_METHOD']);
  788. } finally {
  789. putenv('no_proxy');
  790. unset($_SERVER['no_proxy']);
  791. }
  792. }
  793. /**
  794. * @requires extension zlib
  795. */
  796. public function testAutoEncodingRequest()
  797. {
  798. $client = $this->getHttpClient(__FUNCTION__);
  799. $response = $client->request('GET', 'http://localhost:8057');
  800. $this->assertSame(200, $response->getStatusCode());
  801. $headers = $response->getHeaders();
  802. $this->assertSame(['Accept-Encoding'], $headers['vary']);
  803. $this->assertStringContainsString('gzip', $headers['content-encoding'][0]);
  804. $body = $response->toArray();
  805. $this->assertStringContainsString('gzip', $body['HTTP_ACCEPT_ENCODING']);
  806. }
  807. public function testBaseUri()
  808. {
  809. $client = $this->getHttpClient(__FUNCTION__);
  810. $response = $client->request('GET', '../404', [
  811. 'base_uri' => 'http://localhost:8057/abc/',
  812. ]);
  813. $this->assertSame(404, $response->getStatusCode());
  814. $this->assertSame(['application/json'], $response->getHeaders(false)['content-type']);
  815. }
  816. public function testQuery()
  817. {
  818. $client = $this->getHttpClient(__FUNCTION__);
  819. $response = $client->request('GET', 'http://localhost:8057/?a=a', [
  820. 'query' => ['b' => 'b'],
  821. ]);
  822. $body = $response->toArray();
  823. $this->assertSame('GET', $body['REQUEST_METHOD']);
  824. $this->assertSame('/?a=a&b=b', $body['REQUEST_URI']);
  825. }
  826. public function testInformationalResponse()
  827. {
  828. $client = $this->getHttpClient(__FUNCTION__);
  829. $response = $client->request('GET', 'http://localhost:8057/103');
  830. $this->assertSame('Here the body', $response->getContent());
  831. $this->assertSame(200, $response->getStatusCode());
  832. }
  833. public function testInformationalResponseStream()
  834. {
  835. $client = $this->getHttpClient(__FUNCTION__);
  836. $response = $client->request('GET', 'http://localhost:8057/103');
  837. $chunks = [];
  838. foreach ($client->stream($response) as $chunk) {
  839. $chunks[] = $chunk;
  840. }
  841. $this->assertSame(103, $chunks[0]->getInformationalStatus()[0]);
  842. $this->assertSame(['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script'], $chunks[0]->getInformationalStatus()[1]['link']);
  843. $this->assertTrue($chunks[1]->isFirst());
  844. $this->assertSame('Here the body', $chunks[2]->getContent());
  845. $this->assertTrue($chunks[3]->isLast());
  846. $this->assertNull($chunks[3]->getInformationalStatus());
  847. $this->assertSame(['date', 'content-length'], array_keys($response->getHeaders()));
  848. $this->assertContains('Link: </style.css>; rel=preload; as=style', $response->getInfo('response_headers'));
  849. }
  850. /**
  851. * @requires extension zlib
  852. */
  853. public function testUserlandEncodingRequest()
  854. {
  855. $client = $this->getHttpClient(__FUNCTION__);
  856. $response = $client->request('GET', 'http://localhost:8057', [
  857. 'headers' => ['Accept-Encoding' => 'gzip'],
  858. ]);
  859. $headers = $response->getHeaders();
  860. $this->assertSame(['Accept-Encoding'], $headers['vary']);
  861. $this->assertStringContainsString('gzip', $headers['content-encoding'][0]);
  862. $body = $response->getContent();
  863. $this->assertSame("\x1F", $body[0]);
  864. $body = json_decode(gzdecode($body), true);
  865. $this->assertSame('gzip', $body['HTTP_ACCEPT_ENCODING']);
  866. }
  867. /**
  868. * @requires extension zlib
  869. */
  870. public function testGzipBroken()
  871. {
  872. $client = $this->getHttpClient(__FUNCTION__);
  873. $response = $client->request('GET', 'http://localhost:8057/gzip-broken');
  874. $this->expectException(TransportExceptionInterface::class);
  875. $response->getContent();
  876. }
  877. public function testMaxDuration()
  878. {
  879. $client = $this->getHttpClient(__FUNCTION__);
  880. $response = $client->request('GET', 'http://localhost:8057/max-duration', [
  881. 'max_duration' => 0.1,
  882. ]);
  883. $start = microtime(true);
  884. try {
  885. $response->getContent();
  886. } catch (TransportExceptionInterface $e) {
  887. $this->addToAssertionCount(1);
  888. }
  889. $duration = microtime(true) - $start;
  890. $this->assertLessThan(10, $duration);
  891. }
  892. public function testWithOptions()
  893. {
  894. $client = $this->getHttpClient(__FUNCTION__);
  895. if (!method_exists($client, 'withOptions')) {
  896. $this->markTestSkipped(sprintf('Not implementing "%s::withOptions()" is deprecated.', get_debug_type($client)));
  897. }
  898. $client2 = $client->withOptions(['base_uri' => 'http://localhost:8057/']);
  899. $this->assertNotSame($client, $client2);
  900. $this->assertSame(\get_class($client), \get_class($client2));
  901. $response = $client2->request('GET', '/');
  902. $this->assertSame(200, $response->getStatusCode());
  903. }
  904. }