HttpClientTestCase.php 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137
  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. /**
  259. * @testWith [[]]
  260. * [["Content-Length: 7"]]
  261. */
  262. public function testRedirects(array $headers = [])
  263. {
  264. $client = $this->getHttpClient(__FUNCTION__);
  265. $response = $client->request('POST', 'http://localhost:8057/301', [
  266. 'auth_basic' => 'foo:bar',
  267. 'headers' => $headers,
  268. 'body' => function () {
  269. yield 'foo=bar';
  270. },
  271. ]);
  272. $body = $response->toArray();
  273. $this->assertSame('GET', $body['REQUEST_METHOD']);
  274. $this->assertSame('Basic Zm9vOmJhcg==', $body['HTTP_AUTHORIZATION']);
  275. $this->assertSame('http://localhost:8057/', $response->getInfo('url'));
  276. $this->assertSame(2, $response->getInfo('redirect_count'));
  277. $this->assertNull($response->getInfo('redirect_url'));
  278. $expected = [
  279. 'HTTP/1.1 301 Moved Permanently',
  280. 'Location: http://127.0.0.1:8057/302',
  281. 'Content-Type: application/json',
  282. 'HTTP/1.1 302 Found',
  283. 'Location: http://localhost:8057/',
  284. 'Content-Type: application/json',
  285. 'HTTP/1.1 200 OK',
  286. 'Content-Type: application/json',
  287. ];
  288. $filteredHeaders = array_values(array_filter($response->getInfo('response_headers'), function ($h) {
  289. return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true) && 'Content-Encoding: gzip' !== $h;
  290. }));
  291. $this->assertSame($expected, $filteredHeaders);
  292. }
  293. public function testInvalidRedirect()
  294. {
  295. $client = $this->getHttpClient(__FUNCTION__);
  296. $response = $client->request('GET', 'http://localhost:8057/301/invalid');
  297. $this->assertSame(301, $response->getStatusCode());
  298. $this->assertSame(['//?foo=bar'], $response->getHeaders(false)['location']);
  299. $this->assertSame(0, $response->getInfo('redirect_count'));
  300. $this->assertNull($response->getInfo('redirect_url'));
  301. $this->expectException(RedirectionExceptionInterface::class);
  302. $response->getHeaders();
  303. }
  304. public function testRelativeRedirects()
  305. {
  306. $client = $this->getHttpClient(__FUNCTION__);
  307. $response = $client->request('GET', 'http://localhost:8057/302/relative');
  308. $body = $response->toArray();
  309. $this->assertSame('/', $body['REQUEST_URI']);
  310. $this->assertNull($response->getInfo('redirect_url'));
  311. $response = $client->request('GET', 'http://localhost:8057/302/relative', [
  312. 'max_redirects' => 0,
  313. ]);
  314. $this->assertSame(302, $response->getStatusCode());
  315. $this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));
  316. }
  317. public function testRedirect307()
  318. {
  319. $client = $this->getHttpClient(__FUNCTION__);
  320. $response = $client->request('POST', 'http://localhost:8057/307', [
  321. 'body' => function () {
  322. yield 'foo=bar';
  323. },
  324. 'max_redirects' => 0,
  325. ]);
  326. $this->assertSame(307, $response->getStatusCode());
  327. $response = $client->request('POST', 'http://localhost:8057/307', [
  328. 'body' => 'foo=bar',
  329. ]);
  330. $body = $response->toArray();
  331. $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
  332. }
  333. public function testMaxRedirects()
  334. {
  335. $client = $this->getHttpClient(__FUNCTION__);
  336. $response = $client->request('GET', 'http://localhost:8057/301', [
  337. 'max_redirects' => 1,
  338. 'auth_basic' => 'foo:bar',
  339. ]);
  340. try {
  341. $response->getHeaders();
  342. $this->fail(RedirectionExceptionInterface::class.' expected');
  343. } catch (RedirectionExceptionInterface $e) {
  344. }
  345. $this->assertSame(302, $response->getStatusCode());
  346. $this->assertSame(1, $response->getInfo('redirect_count'));
  347. $this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));
  348. $expected = [
  349. 'HTTP/1.1 301 Moved Permanently',
  350. 'Location: http://127.0.0.1:8057/302',
  351. 'Content-Type: application/json',
  352. 'HTTP/1.1 302 Found',
  353. 'Location: http://localhost:8057/',
  354. 'Content-Type: application/json',
  355. ];
  356. $filteredHeaders = array_values(array_filter($response->getInfo('response_headers'), function ($h) {
  357. return \in_array(substr($h, 0, 4), ['HTTP', 'Loca', 'Cont'], true);
  358. }));
  359. $this->assertSame($expected, $filteredHeaders);
  360. }
  361. public function testStream()
  362. {
  363. $client = $this->getHttpClient(__FUNCTION__);
  364. $response = $client->request('GET', 'http://localhost:8057');
  365. $chunks = $client->stream($response);
  366. $result = [];
  367. foreach ($chunks as $r => $chunk) {
  368. if ($chunk->isTimeout()) {
  369. $result[] = 't';
  370. } elseif ($chunk->isLast()) {
  371. $result[] = 'l';
  372. } elseif ($chunk->isFirst()) {
  373. $result[] = 'f';
  374. }
  375. }
  376. $this->assertSame($response, $r);
  377. $this->assertSame(['f', 'l'], $result);
  378. $chunk = null;
  379. $i = 0;
  380. foreach ($client->stream($response) as $chunk) {
  381. ++$i;
  382. }
  383. $this->assertSame(1, $i);
  384. $this->assertTrue($chunk->isLast());
  385. }
  386. public function testAddToStream()
  387. {
  388. $client = $this->getHttpClient(__FUNCTION__);
  389. $r1 = $client->request('GET', 'http://localhost:8057');
  390. $completed = [];
  391. $pool = [$r1];
  392. while ($pool) {
  393. $chunks = $client->stream($pool);
  394. $pool = [];
  395. foreach ($chunks as $r => $chunk) {
  396. if (!$chunk->isLast()) {
  397. continue;
  398. }
  399. if ($r1 === $r) {
  400. $r2 = $client->request('GET', 'http://localhost:8057');
  401. $pool[] = $r2;
  402. }
  403. $completed[] = $r;
  404. }
  405. }
  406. $this->assertSame([$r1, $r2], $completed);
  407. }
  408. public function testCompleteTypeError()
  409. {
  410. $client = $this->getHttpClient(__FUNCTION__);
  411. $this->expectException(\TypeError::class);
  412. $client->stream(123);
  413. }
  414. public function testOnProgress()
  415. {
  416. $client = $this->getHttpClient(__FUNCTION__);
  417. $response = $client->request('POST', 'http://localhost:8057/post', [
  418. 'headers' => ['Content-Length' => 14],
  419. 'body' => 'foo=0123456789',
  420. 'on_progress' => function (...$state) use (&$steps) { $steps[] = $state; },
  421. ]);
  422. $body = $response->toArray();
  423. $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
  424. $this->assertSame([0, 0], \array_slice($steps[0], 0, 2));
  425. $lastStep = \array_slice($steps, -1)[0];
  426. $this->assertSame([57, 57], \array_slice($lastStep, 0, 2));
  427. $this->assertSame('http://localhost:8057/post', $steps[0][2]['url']);
  428. }
  429. public function testPostJson()
  430. {
  431. $client = $this->getHttpClient(__FUNCTION__);
  432. $response = $client->request('POST', 'http://localhost:8057/post', [
  433. 'json' => ['foo' => 'bar'],
  434. ]);
  435. $body = $response->toArray();
  436. $this->assertStringContainsString('json', $body['content-type']);
  437. unset($body['content-type']);
  438. $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $body);
  439. }
  440. public function testPostArray()
  441. {
  442. $client = $this->getHttpClient(__FUNCTION__);
  443. $response = $client->request('POST', 'http://localhost:8057/post', [
  444. 'body' => ['foo' => 'bar'],
  445. ]);
  446. $this->assertSame(['foo' => 'bar', 'REQUEST_METHOD' => 'POST'], $response->toArray());
  447. }
  448. public function testPostResource()
  449. {
  450. $client = $this->getHttpClient(__FUNCTION__);
  451. $h = fopen('php://temp', 'w+');
  452. fwrite($h, 'foo=0123456789');
  453. rewind($h);
  454. $response = $client->request('POST', 'http://localhost:8057/post', [
  455. 'body' => $h,
  456. ]);
  457. $body = $response->toArray();
  458. $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $body);
  459. }
  460. public function testPostCallback()
  461. {
  462. $client = $this->getHttpClient(__FUNCTION__);
  463. $response = $client->request('POST', 'http://localhost:8057/post', [
  464. 'body' => function () {
  465. yield 'foo';
  466. yield '';
  467. yield '=';
  468. yield '0123456789';
  469. },
  470. ]);
  471. $this->assertSame(['foo' => '0123456789', 'REQUEST_METHOD' => 'POST'], $response->toArray());
  472. }
  473. public function testCancel()
  474. {
  475. $client = $this->getHttpClient(__FUNCTION__);
  476. $response = $client->request('GET', 'http://localhost:8057/timeout-header');
  477. $response->cancel();
  478. $this->expectException(TransportExceptionInterface::class);
  479. $response->getHeaders();
  480. }
  481. public function testInfoOnCanceledResponse()
  482. {
  483. $client = $this->getHttpClient(__FUNCTION__);
  484. $response = $client->request('GET', 'http://localhost:8057/timeout-header');
  485. $this->assertFalse($response->getInfo('canceled'));
  486. $response->cancel();
  487. $this->assertTrue($response->getInfo('canceled'));
  488. }
  489. public function testCancelInStream()
  490. {
  491. $client = $this->getHttpClient(__FUNCTION__);
  492. $response = $client->request('GET', 'http://localhost:8057/404');
  493. foreach ($client->stream($response) as $chunk) {
  494. $response->cancel();
  495. }
  496. $this->expectException(TransportExceptionInterface::class);
  497. foreach ($client->stream($response) as $chunk) {
  498. }
  499. }
  500. public function testOnProgressCancel()
  501. {
  502. $client = $this->getHttpClient(__FUNCTION__);
  503. $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
  504. 'on_progress' => function ($dlNow) {
  505. if (0 < $dlNow) {
  506. throw new \Exception('Aborting the request.');
  507. }
  508. },
  509. ]);
  510. try {
  511. foreach ($client->stream([$response]) as $chunk) {
  512. }
  513. $this->fail(ClientExceptionInterface::class.' expected');
  514. } catch (TransportExceptionInterface $e) {
  515. $this->assertSame('Aborting the request.', $e->getPrevious()->getMessage());
  516. }
  517. $this->assertNotNull($response->getInfo('error'));
  518. $this->expectException(TransportExceptionInterface::class);
  519. $response->getContent();
  520. }
  521. public function testOnProgressError()
  522. {
  523. $client = $this->getHttpClient(__FUNCTION__);
  524. $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
  525. 'on_progress' => function ($dlNow) {
  526. if (0 < $dlNow) {
  527. throw new \Error('BUG.');
  528. }
  529. },
  530. ]);
  531. try {
  532. foreach ($client->stream([$response]) as $chunk) {
  533. }
  534. $this->fail('Error expected');
  535. } catch (\Error $e) {
  536. $this->assertSame('BUG.', $e->getMessage());
  537. }
  538. $this->assertNotNull($response->getInfo('error'));
  539. $this->expectException(TransportExceptionInterface::class);
  540. $response->getContent();
  541. }
  542. public function testResolve()
  543. {
  544. $client = $this->getHttpClient(__FUNCTION__);
  545. $response = $client->request('GET', 'http://symfony.com:8057/', [
  546. 'resolve' => ['symfony.com' => '127.0.0.1'],
  547. ]);
  548. $this->assertSame(200, $response->getStatusCode());
  549. $this->assertSame(200, $client->request('GET', 'http://symfony.com:8057/')->getStatusCode());
  550. $response = null;
  551. $this->expectException(TransportExceptionInterface::class);
  552. $client->request('GET', 'http://symfony.com:8057/', ['timeout' => 1]);
  553. }
  554. public function testIdnResolve()
  555. {
  556. $client = $this->getHttpClient(__FUNCTION__);
  557. $response = $client->request('GET', 'http://0-------------------------------------------------------------0.com:8057/', [
  558. 'resolve' => ['0-------------------------------------------------------------0.com' => '127.0.0.1'],
  559. ]);
  560. $this->assertSame(200, $response->getStatusCode());
  561. $response = $client->request('GET', 'http://Bücher.example:8057/', [
  562. 'resolve' => ['xn--bcher-kva.example' => '127.0.0.1'],
  563. ]);
  564. $this->assertSame(200, $response->getStatusCode());
  565. }
  566. public function testNotATimeout()
  567. {
  568. $client = $this->getHttpClient(__FUNCTION__);
  569. $response = $client->request('GET', 'http://localhost:8057/timeout-header', [
  570. 'timeout' => 0.9,
  571. ]);
  572. sleep(1);
  573. $this->assertSame(200, $response->getStatusCode());
  574. }
  575. public function testTimeoutOnAccess()
  576. {
  577. $client = $this->getHttpClient(__FUNCTION__);
  578. $response = $client->request('GET', 'http://localhost:8057/timeout-header', [
  579. 'timeout' => 0.1,
  580. ]);
  581. $this->expectException(TransportExceptionInterface::class);
  582. $response->getHeaders();
  583. }
  584. public function testTimeoutIsNotAFatalError()
  585. {
  586. usleep(300000); // wait for the previous test to release the server
  587. $client = $this->getHttpClient(__FUNCTION__);
  588. $response = $client->request('GET', 'http://localhost:8057/timeout-body', [
  589. 'timeout' => 0.25,
  590. ]);
  591. try {
  592. $response->getContent();
  593. $this->fail(TimeoutExceptionInterface::class.' expected');
  594. } catch (TimeoutExceptionInterface $e) {
  595. }
  596. for ($i = 0; $i < 10; ++$i) {
  597. try {
  598. $this->assertSame('<1><2>', $response->getContent());
  599. break;
  600. } catch (TimeoutExceptionInterface $e) {
  601. }
  602. }
  603. if (10 === $i) {
  604. throw $e;
  605. }
  606. }
  607. public function testTimeoutOnStream()
  608. {
  609. $client = $this->getHttpClient(__FUNCTION__);
  610. $response = $client->request('GET', 'http://localhost:8057/timeout-body');
  611. $this->assertSame(200, $response->getStatusCode());
  612. $chunks = $client->stream([$response], 0.2);
  613. $result = [];
  614. foreach ($chunks as $r => $chunk) {
  615. if ($chunk->isTimeout()) {
  616. $result[] = 't';
  617. } else {
  618. $result[] = $chunk->getContent();
  619. }
  620. }
  621. $this->assertSame(['<1>', 't'], $result);
  622. $chunks = $client->stream([$response]);
  623. foreach ($chunks as $r => $chunk) {
  624. $this->assertSame('<2>', $chunk->getContent());
  625. $this->assertSame('<1><2>', $r->getContent());
  626. return;
  627. }
  628. $this->fail('The response should have completed');
  629. }
  630. public function testUncheckedTimeoutThrows()
  631. {
  632. $client = $this->getHttpClient(__FUNCTION__);
  633. $response = $client->request('GET', 'http://localhost:8057/timeout-body');
  634. $chunks = $client->stream([$response], 0.1);
  635. $this->expectException(TransportExceptionInterface::class);
  636. foreach ($chunks as $r => $chunk) {
  637. }
  638. }
  639. public function testTimeoutWithActiveConcurrentStream()
  640. {
  641. $p1 = TestHttpServer::start(8067);
  642. $p2 = TestHttpServer::start(8077);
  643. $client = $this->getHttpClient(__FUNCTION__);
  644. $streamingResponse = $client->request('GET', 'http://localhost:8067/max-duration');
  645. $blockingResponse = $client->request('GET', 'http://localhost:8077/timeout-body', [
  646. 'timeout' => 0.25,
  647. ]);
  648. $this->assertSame(200, $streamingResponse->getStatusCode());
  649. $this->assertSame(200, $blockingResponse->getStatusCode());
  650. $this->expectException(TransportExceptionInterface::class);
  651. try {
  652. $blockingResponse->getContent();
  653. } finally {
  654. $p1->stop();
  655. $p2->stop();
  656. }
  657. }
  658. public function testTimeoutOnInitialize()
  659. {
  660. $p1 = TestHttpServer::start(8067);
  661. $p2 = TestHttpServer::start(8077);
  662. $client = $this->getHttpClient(__FUNCTION__);
  663. $start = microtime(true);
  664. $responses = [];
  665. $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
  666. $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
  667. $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
  668. $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
  669. try {
  670. foreach ($responses as $response) {
  671. try {
  672. $response->getContent();
  673. $this->fail(TransportExceptionInterface::class.' expected');
  674. } catch (TransportExceptionInterface $e) {
  675. }
  676. }
  677. $responses = [];
  678. $duration = microtime(true) - $start;
  679. $this->assertLessThan(1.0, $duration);
  680. } finally {
  681. $p1->stop();
  682. $p2->stop();
  683. }
  684. }
  685. public function testTimeoutOnDestruct()
  686. {
  687. $p1 = TestHttpServer::start(8067);
  688. $p2 = TestHttpServer::start(8077);
  689. $client = $this->getHttpClient(__FUNCTION__);
  690. $start = microtime(true);
  691. $responses = [];
  692. $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
  693. $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
  694. $responses[] = $client->request('GET', 'http://localhost:8067/timeout-header', ['timeout' => 0.25]);
  695. $responses[] = $client->request('GET', 'http://localhost:8077/timeout-header', ['timeout' => 0.25]);
  696. try {
  697. while ($response = array_shift($responses)) {
  698. try {
  699. unset($response);
  700. $this->fail(TransportExceptionInterface::class.' expected');
  701. } catch (TransportExceptionInterface $e) {
  702. }
  703. }
  704. $duration = microtime(true) - $start;
  705. $this->assertLessThan(1.0, $duration);
  706. } finally {
  707. $p1->stop();
  708. $p2->stop();
  709. }
  710. }
  711. public function testDestruct()
  712. {
  713. $client = $this->getHttpClient(__FUNCTION__);
  714. $start = microtime(true);
  715. $client->request('GET', 'http://localhost:8057/timeout-long');
  716. $client = null;
  717. $duration = microtime(true) - $start;
  718. $this->assertGreaterThan(1, $duration);
  719. $this->assertLessThan(4, $duration);
  720. }
  721. public function testGetContentAfterDestruct()
  722. {
  723. $client = $this->getHttpClient(__FUNCTION__);
  724. try {
  725. $client->request('GET', 'http://localhost:8057/404');
  726. $this->fail(ClientExceptionInterface::class.' expected');
  727. } catch (ClientExceptionInterface $e) {
  728. $this->assertSame('GET', $e->getResponse()->toArray(false)['REQUEST_METHOD']);
  729. }
  730. }
  731. public function testGetEncodedContentAfterDestruct()
  732. {
  733. $client = $this->getHttpClient(__FUNCTION__);
  734. try {
  735. $client->request('GET', 'http://localhost:8057/404-gzipped');
  736. $this->fail(ClientExceptionInterface::class.' expected');
  737. } catch (ClientExceptionInterface $e) {
  738. $this->assertSame('some text', $e->getResponse()->getContent(false));
  739. }
  740. }
  741. public function testProxy()
  742. {
  743. $client = $this->getHttpClient(__FUNCTION__);
  744. $response = $client->request('GET', 'http://localhost:8057/', [
  745. 'proxy' => 'http://localhost:8057',
  746. ]);
  747. $body = $response->toArray();
  748. $this->assertSame('localhost:8057', $body['HTTP_HOST']);
  749. $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
  750. $response = $client->request('GET', 'http://localhost:8057/', [
  751. 'proxy' => 'http://foo:b%3Dar@localhost:8057',
  752. ]);
  753. $body = $response->toArray();
  754. $this->assertSame('Basic Zm9vOmI9YXI=', $body['HTTP_PROXY_AUTHORIZATION']);
  755. $_SERVER['http_proxy'] = 'http://localhost:8057';
  756. try {
  757. $response = $client->request('GET', 'http://localhost:8057/');
  758. $body = $response->toArray();
  759. $this->assertSame('localhost:8057', $body['HTTP_HOST']);
  760. $this->assertMatchesRegularExpression('#^http://(localhost|127\.0\.0\.1):8057/$#', $body['REQUEST_URI']);
  761. } finally {
  762. unset($_SERVER['http_proxy']);
  763. }
  764. }
  765. public function testNoProxy()
  766. {
  767. putenv('no_proxy='.$_SERVER['no_proxy'] = 'example.com, localhost');
  768. try {
  769. $client = $this->getHttpClient(__FUNCTION__);
  770. $response = $client->request('GET', 'http://localhost:8057/', [
  771. 'proxy' => 'http://localhost:8057',
  772. ]);
  773. $body = $response->toArray();
  774. $this->assertSame('HTTP/1.1', $body['SERVER_PROTOCOL']);
  775. $this->assertSame('/', $body['REQUEST_URI']);
  776. $this->assertSame('GET', $body['REQUEST_METHOD']);
  777. } finally {
  778. putenv('no_proxy');
  779. unset($_SERVER['no_proxy']);
  780. }
  781. }
  782. /**
  783. * @requires extension zlib
  784. */
  785. public function testAutoEncodingRequest()
  786. {
  787. $client = $this->getHttpClient(__FUNCTION__);
  788. $response = $client->request('GET', 'http://localhost:8057');
  789. $this->assertSame(200, $response->getStatusCode());
  790. $headers = $response->getHeaders();
  791. $this->assertSame(['Accept-Encoding'], $headers['vary']);
  792. $this->assertStringContainsString('gzip', $headers['content-encoding'][0]);
  793. $body = $response->toArray();
  794. $this->assertStringContainsString('gzip', $body['HTTP_ACCEPT_ENCODING']);
  795. }
  796. public function testBaseUri()
  797. {
  798. $client = $this->getHttpClient(__FUNCTION__);
  799. $response = $client->request('GET', '../404', [
  800. 'base_uri' => 'http://localhost:8057/abc/',
  801. ]);
  802. $this->assertSame(404, $response->getStatusCode());
  803. $this->assertSame(['application/json'], $response->getHeaders(false)['content-type']);
  804. }
  805. public function testQuery()
  806. {
  807. $client = $this->getHttpClient(__FUNCTION__);
  808. $response = $client->request('GET', 'http://localhost:8057/?a=a', [
  809. 'query' => ['b' => 'b'],
  810. ]);
  811. $body = $response->toArray();
  812. $this->assertSame('GET', $body['REQUEST_METHOD']);
  813. $this->assertSame('/?a=a&b=b', $body['REQUEST_URI']);
  814. }
  815. public function testInformationalResponse()
  816. {
  817. $client = $this->getHttpClient(__FUNCTION__);
  818. $response = $client->request('GET', 'http://localhost:8057/103');
  819. $this->assertSame('Here the body', $response->getContent());
  820. $this->assertSame(200, $response->getStatusCode());
  821. }
  822. public function testInformationalResponseStream()
  823. {
  824. $client = $this->getHttpClient(__FUNCTION__);
  825. $response = $client->request('GET', 'http://localhost:8057/103');
  826. $chunks = [];
  827. foreach ($client->stream($response) as $chunk) {
  828. $chunks[] = $chunk;
  829. }
  830. $this->assertSame(103, $chunks[0]->getInformationalStatus()[0]);
  831. $this->assertSame(['</style.css>; rel=preload; as=style', '</script.js>; rel=preload; as=script'], $chunks[0]->getInformationalStatus()[1]['link']);
  832. $this->assertTrue($chunks[1]->isFirst());
  833. $this->assertSame('Here the body', $chunks[2]->getContent());
  834. $this->assertTrue($chunks[3]->isLast());
  835. $this->assertNull($chunks[3]->getInformationalStatus());
  836. $this->assertSame(['date', 'content-length'], array_keys($response->getHeaders()));
  837. $this->assertContains('Link: </style.css>; rel=preload; as=style', $response->getInfo('response_headers'));
  838. }
  839. /**
  840. * @requires extension zlib
  841. */
  842. public function testUserlandEncodingRequest()
  843. {
  844. $client = $this->getHttpClient(__FUNCTION__);
  845. $response = $client->request('GET', 'http://localhost:8057', [
  846. 'headers' => ['Accept-Encoding' => 'gzip'],
  847. ]);
  848. $headers = $response->getHeaders();
  849. $this->assertSame(['Accept-Encoding'], $headers['vary']);
  850. $this->assertStringContainsString('gzip', $headers['content-encoding'][0]);
  851. $body = $response->getContent();
  852. $this->assertSame("\x1F", $body[0]);
  853. $body = json_decode(gzdecode($body), true);
  854. $this->assertSame('gzip', $body['HTTP_ACCEPT_ENCODING']);
  855. }
  856. /**
  857. * @requires extension zlib
  858. */
  859. public function testGzipBroken()
  860. {
  861. $client = $this->getHttpClient(__FUNCTION__);
  862. $response = $client->request('GET', 'http://localhost:8057/gzip-broken');
  863. $this->expectException(TransportExceptionInterface::class);
  864. $response->getContent();
  865. }
  866. public function testMaxDuration()
  867. {
  868. $client = $this->getHttpClient(__FUNCTION__);
  869. $response = $client->request('GET', 'http://localhost:8057/max-duration', [
  870. 'max_duration' => 0.1,
  871. ]);
  872. $start = microtime(true);
  873. try {
  874. $response->getContent();
  875. } catch (TransportExceptionInterface $e) {
  876. $this->addToAssertionCount(1);
  877. }
  878. $duration = microtime(true) - $start;
  879. $this->assertLessThan(10, $duration);
  880. }
  881. public function testWithOptions()
  882. {
  883. $client = $this->getHttpClient(__FUNCTION__);
  884. if (!method_exists($client, 'withOptions')) {
  885. $this->markTestSkipped(sprintf('Not implementing "%s::withOptions()" is deprecated.', get_debug_type($client)));
  886. }
  887. $client2 = $client->withOptions(['base_uri' => 'http://localhost:8057/']);
  888. $this->assertNotSame($client, $client2);
  889. $this->assertSame(\get_class($client), \get_class($client2));
  890. $response = $client2->request('GET', '/');
  891. $this->assertSame(200, $response->getStatusCode());
  892. }
  893. }