You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1057 lines
34 KiB

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