HttpClientTestCase.php 37 KB

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