├── .github └── FUNDING.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Browser.php ├── Io ├── ChunkedEncoder.php ├── Sender.php └── Transaction.php └── Message ├── MessageFactory.php ├── ReadableBodyStream.php └── ResponseException.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: clue 2 | custom: https://clue.engineering/support 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 2.9.0 (2020-07-03) 4 | 5 | A **major feature release** adding a new options APIs and more consistent APIs 6 | for sending streaming requests. Includes a major documentation overhaul and 7 | deprecates a number of APIs. 8 | 9 | * Feature: Add new `request()` and `requestStreaming()` methods and 10 | deprecate `send()` method and `streaming` option. 11 | (#170 by @clue) 12 | 13 | ```php 14 | // old: deprecated 15 | $browser->withOptions(['streaming' => true])->get($url); 16 | $browser->send(new Request('OPTIONS', $url)); 17 | 18 | // new 19 | $browser->requestStreaming('GET', $url); 20 | $browser->request('OPTIONS', $url); 21 | ``` 22 | 23 | * Feature: Add dedicated methods to control options, deprecate `withOptions()`. 24 | (#172 by @clue) 25 | 26 | ```php 27 | // old: deprecated 28 | $browser->withOptions(['timeout' => 10]); 29 | $browser->withOptions(['followRedirects' => false]); 30 | $browser->withOptions(['obeySuccessCode' => false]); 31 | 32 | // new 33 | $browser->withTimeout(10); 34 | $browser->withFollowRedirects(false); 35 | $browser->withRejectErrorResponse(false); 36 | ``` 37 | 38 | * Feature: Add `withResponseBuffer()` method to limit maximum response buffer size (defaults to 16 MiB). 39 | (#175 by @clue) 40 | 41 | ```php 42 | // new: download maximum of 100 MB 43 | $browser->withResponseBuffer(100 * 1000000)->get($url); 44 | ``` 45 | 46 | * Feature: Improve `withBase()` method and deprecate `withoutBase()` method 47 | (#173 by @clue) 48 | 49 | ```php 50 | // old: deprecated 51 | $browser = $browser->withoutBase(); 52 | 53 | // new 54 | $browser = $browser->withBase(null); 55 | ``` 56 | 57 | * Deprecate `submit()` method, use `post()` instead. 58 | (#171 by @clue) 59 | 60 | ```php 61 | // old: deprecated 62 | $browser->submit($url, $data); 63 | 64 | // new 65 | $browser->post($url, ['Content-Type' => 'application/x-www-form-urlencoded'], http_build_query($data)); 66 | ``` 67 | 68 | * Deprecate `UriInterface` for request methods, use URL strings instead 69 | (#174 by @clue) 70 | 71 | * Fix: Fix unneeded timeout timer when request body closes and sender already rejected. 72 | (#169 by @clue) 73 | 74 | * Improve documentation structure, add documentation for all API methods and 75 | handling concurrency. 76 | (#167 and #176 by @clue) 77 | 78 | * Improve test suite to use ReactPHP-based webserver instead of httpbin and 79 | add forward compatibility with PHPUnit 9. 80 | (#168 by @clue) 81 | 82 | ## 2.8.2 (2020-06-02) 83 | 84 | * Fix: HTTP `HEAD` requests should not expect a response body. 85 | (#166 by @clue) 86 | 87 | ## 2.8.1 (2020-05-19) 88 | 89 | * Fix: Fix cancellation of pending requests with promise followers. 90 | (#164 by @clue) 91 | 92 | ## 2.8.0 (2020-05-13) 93 | 94 | * Feature: Use HTTP/1.1 protocol version by default and add new `Browser::withProtocolVersion()`. 95 | (#162 by @clue) 96 | 97 | This is the preferred HTTP protocol version which also provides decent 98 | backwards-compatibility with legacy HTTP/1.0 servers. As such, there should 99 | rarely be a need to explicitly change this protocol version. You can revert 100 | to legacy HTTP/1.0 like this: 101 | 102 | ```php 103 | $browser->withProtocolVersion('1.0')->get($url)->then(…); 104 | ``` 105 | 106 | * Feature / Fix: Explicitly close connection after response body ends. 107 | (#161 by @clue) 108 | 109 | This improves support for servers ignoring the `Connection: close` request 110 | header that would otherwise keep the connection open and could eventually 111 | run into a timeout even though the transfer was completed. 112 | 113 | * Fixed small issue in code example. 114 | (#160 by @mmoreram) 115 | 116 | * Clean up test suite and add `.gitattributes` to exclude dev files from exports. 117 | (#163 by @SimonFrings) 118 | 119 | ## 2.7.0 (2020-02-26) 120 | 121 | * Feature: Add backpressure support and support throttling for streaming outgoing chunked request body. 122 | (#148 by @clue) 123 | 124 | * Feature: Start sending outgoing request even when streaming body doesn't emit any data yet. 125 | (#150 by @clue) 126 | 127 | * Feature: Only start request timeout timer after streaming request body has been sent (exclude upload time). 128 | (#151 and #152 by @clue) 129 | 130 | * Feature: Reject request when streaming request body emits error or closes unexpectedly. 131 | (#153 by @clue) 132 | 133 | * Improve download benchmarking script and add new upload benchmark. 134 | (#149 by @clue) 135 | 136 | ## 2.6.1 (2020-01-14) 137 | 138 | * Improve test suite by testing against PHP 7.4 and simplify test setup and test matrix 139 | and fix testing redirected request when following relative redirect. 140 | (#145 and #147 by @clue) 141 | 142 | * Add support / sponsorship info and fix documentation typo. 143 | (#144 by @clue and #133 by @eislambey) 144 | 145 | ## 2.6.0 (2019-04-03) 146 | 147 | * Feature / Fix: Add `Content-Length: 0` request header for empty `POST` request etc. 148 | (#120 by @clue) 149 | 150 | * Fix: Only try to follow redirects if `Location` response header is present. 151 | (#130 by @clue) 152 | 153 | * Documentation and example for SSH proxy (SSH tunnel) and update SOCKS proxy example. 154 | (#116, #119 and #121 by @clue) 155 | 156 | * Improve test suite and also run tests on PHP 7.3. 157 | (#122 by @samnela) 158 | 159 | ## 2.5.0 (2018-10-24) 160 | 161 | * Feature: Add HTTP timeout option. 162 | (#114 by @Rakdar and @clue) 163 | 164 | This now respects PHP's `default_socket_timeout` setting (default 60s) as a 165 | timeout for sending the outgoing HTTP request and waiting for a successful 166 | response and will otherwise cancel the pending request and reject its value 167 | with an Exception. You can now use the [`timeout` option](#withoptions) to 168 | pass a custom timeout value in seconds like this: 169 | 170 | ```php 171 | $browser = $browser->withOptions(array( 172 | 'timeout' => 10.0 173 | )); 174 | 175 | $browser->get($uri)->then(function (ResponseInterface $response) { 176 | // response received within 10 seconds maximum 177 | var_dump($response->getHeaders()); 178 | }); 179 | ``` 180 | 181 | Similarly, you can use a negative timeout value to not apply a timeout at 182 | all or use a `null` value to restore the default handling. 183 | 184 | * Improve documentation for `withOptions()` and 185 | add documentation and example for HTTP CONNECT proxy. 186 | (#111 and #115 by @clue) 187 | 188 | * Refactor `Browser` to reuse single `Transaction` instance internally 189 | which now accepts sending individual requests and their options. 190 | (#113 by @clue) 191 | 192 | ## 2.4.0 (2018-10-02) 193 | 194 | * Feature / Fix: Support cancellation forwarding and cancelling redirected requests. 195 | (#110 by @clue) 196 | 197 | * Feature / Fix: Remove `Authorization` request header for redirected cross-origin requests 198 | and add documentation for HTTP redirects. 199 | (#108 by @clue) 200 | 201 | * Improve API documentation and add documentation for HTTP authentication and `Authorization` header. 202 | (#104 and #109 by @clue) 203 | 204 | * Update project homepage. 205 | (#100 by @clue) 206 | 207 | ## 2.3.0 (2018-02-09) 208 | 209 | * Feature / Fix: Pass custom request headers when following redirects 210 | (#91 by @seregazhuk and #96 by @clue) 211 | 212 | * Support legacy PHP 5.3 through PHP 7.2 and HHVM 213 | (#95 by @clue) 214 | 215 | * Improve documentation 216 | (#87 by @holtkamp and #93 by @seregazhuk) 217 | 218 | * Improve test suite by adding forward compatibility with PHPUnit 5, PHPUnit 6 219 | and PHPUnit 7 and explicitly test HTTP/1.1 protocol version. 220 | (#86 by @carusogabriel and #94 and #97 by @clue) 221 | 222 | ## 2.2.0 (2017-10-24) 223 | 224 | * Feature: Forward compatibility with freshly released react/promise-stream v1.0 225 | (#85 by @WyriHaximus) 226 | 227 | ## 2.1.0 (2017-09-17) 228 | 229 | * Feature: Update minimum required Socket dependency version in order to 230 | support Unix Domain Sockets (UDS) again, 231 | support hosts file on all platforms and 232 | work around sending secure HTTPS requests with PHP < 7.1.4 233 | (#84 by @clue) 234 | 235 | ## 2.0.0 (2017-09-16) 236 | 237 | A major compatibility release to update this component to support all latest 238 | ReactPHP components! 239 | 240 | This update involves a minor BC break due to dropped support for legacy 241 | versions. We've tried hard to avoid BC breaks where possible and minimize impact 242 | otherwise. We expect that most consumers of this package will actually not be 243 | affected by any BC breaks, see below for more details. 244 | 245 | * BC break: Remove deprecated API and mark Sender as @internal only, 246 | remove all references to legacy SocketClient component and 247 | remove support for Unix domain sockets (UDS) for now 248 | (#77, #78, #81 and #83 by @clue) 249 | 250 | > All of this affects the `Sender` only, which was previously marked as 251 | "advanced usage" and is now marked `@internal` only. If you've not 252 | used this class before, then this BC break will not affect you. 253 | If you've previously used this class, then it's recommended to first 254 | update to the intermediary v1.4.0 release, which allows you to use a 255 | standard `ConnectorInterface` instead of the `Sender` and then update 256 | to this version without causing a BC break. 257 | If you've previously used Unix domain sockets (UDS), then you're 258 | recommended to wait for the next version. 259 | 260 | * Feature / BC break: Forward compatibility with future Stream v1.0 and strict stream semantics 261 | (#79 by @clue) 262 | 263 | > This component now follows strict stream semantics. This is marked as a 264 | BC break because this removes undocumented and untested excessive event 265 | arguments. If you've relied on proper stream semantics as documented 266 | before, then this BC break will not affect you. 267 | 268 | * Feature: Forward compatibility with future Socket and EventLoop components 269 | (#80 by @clue) 270 | 271 | ## 1.4.0 (2017-09-15) 272 | 273 | * Feature: `Browser` accepts `ConnectorInterface` and deprecate legacy `Sender` 274 | (#76 by @clue) 275 | 276 | If you need custom connector settings (DNS resolution, TLS parameters, timeouts, 277 | proxy servers etc.), you can explicitly pass a custom instance of the 278 | [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): 279 | 280 | ```php 281 | $connector = new \React\Socket\Connector($loop, array( 282 | 'dns' => '127.0.0.1', 283 | 'tcp' => array( 284 | 'bindto' => '192.168.10.1:0' 285 | ), 286 | 'tls' => array( 287 | 'verify_peer' => false, 288 | 'verify_peer_name' => false 289 | ) 290 | )); 291 | 292 | $browser = new Browser($loop, $connector); 293 | ``` 294 | 295 | ## 1.3.0 (2017-09-08) 296 | 297 | * Feature: Support request cancellation 298 | (#75 by @clue) 299 | 300 | ```php 301 | $promise = $browser->get($url); 302 | 303 | $loop->addTimer(2.0, function () use ($promise) { 304 | $promise->cancel(); 305 | }); 306 | ``` 307 | 308 | * Feature: Update react/http-client to v0.5, 309 | support react/stream v0.6 and react/socket-client v0.7 and drop legacy PHP 5.3 support 310 | (#74 by @clue) 311 | 312 | ## 1.2.0 (2017-09-05) 313 | 314 | * Feature: Forward compatibility with react/http-client v0.5 315 | (#72 and #73 by @clue) 316 | 317 | Older HttpClient versions are still supported, but the new version is now 318 | preferred. Advanced usage with custom connectors now recommends setting up 319 | the `React\HttpClient\Client` instance explicitly. 320 | 321 | Accordingly, the `Sender::createFromLoopDns()` and 322 | `Sender::createFromLoopConnectors()` have been marked as deprecated and 323 | will be removed in future versions. 324 | 325 | ## 1.1.1 (2017-09-05) 326 | 327 | * Restructure examples to ease getting started and 328 | fix online tests and add option to exclude tests against httpbin.org 329 | (#67 and #71 by @clue) 330 | 331 | * Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and 332 | lock Travis distro so new defaults will not break the build 333 | (#68 and #70 by @clue) 334 | 335 | ## 1.1.0 (2016-10-21) 336 | 337 | * Feature: Obey explicitly set HTTP protocol version for outgoing requests 338 | (#58, #59 by @WyriHaximus, @clue) 339 | 340 | ```php 341 | $request = new Request('GET', $url); 342 | $request = $request->withProtocolVersion(1.1); 343 | 344 | $browser->send($request)->then(…); 345 | ``` 346 | 347 | ## 1.0.1 (2016-08-12) 348 | 349 | * Fix: Explicitly define all minimum required package versions 350 | (#57 by @clue) 351 | 352 | ## 1.0.0 (2016-08-09) 353 | 354 | * First stable release, now following SemVer 355 | 356 | * Improve documentation and usage examples 357 | 358 | > Contains no other changes, so it's actually fully compatible with the v0.5.0 release. 359 | 360 | ## 0.5.0 (2016-04-02) 361 | 362 | * Feature / BC break: Implement PSR-7 http-message interfaces 363 | (#54 by @clue) 364 | 365 | Replace custom `Message`, `Request`, `Response` and `Uri` classes with 366 | common PSR-7 interfaces: 367 | 368 | ```php 369 | // old 370 | $browser->get($uri)->then(function (Response $response) { 371 | echo 'Test: ' . $response->getHeader('X-Test'); 372 | echo 'Body: ' . $response->getBody(); 373 | }); 374 | 375 | // new 376 | $browser->get($uri)->then(function (ResponseInterface $response) { 377 | if ($response->hasHeader('X-Test')) { 378 | echo 'Test: ' . $response->getHeaderLine('X-Test'); 379 | } 380 | echo 'Body: ' . $response->getBody(); 381 | }); 382 | ``` 383 | 384 | * Feature: Add streaming API 385 | (#56 by @clue) 386 | 387 | ```php 388 | $browser = $browser->withOptions(array('streaming' => true)); 389 | $browser->get($uri)->then(function (ResponseInterface $response) { 390 | $response->getBody()->on('data', function($chunk) { 391 | echo $chunk . PHP_EOL; 392 | }); 393 | }); 394 | ``` 395 | 396 | * Remove / BC break: Remove `Browser::resolve()` because it's now fully decoupled 397 | (#55 by @clue) 398 | 399 | If you need this feature, consider explicitly depending on rize/uri-template 400 | instead: 401 | 402 | ```bash 403 | $ composer require rize/uri-template 404 | ``` 405 | 406 | * Use clue/block-react and new Promise API in order to simplify tests 407 | (#53 by @clue) 408 | 409 | ## 0.4.2 (2016-03-25) 410 | 411 | * Support advanced connection options with newest SocketClient (TLS/HTTPS and socket options) 412 | (#51 by @clue) 413 | 414 | * First class support for PHP 5.3 through PHP 7 and HHVM 415 | (#52 by @clue) 416 | 417 | ## 0.4.1 (2015-09-05) 418 | 419 | * Fix: Replace URI placeholders before applying base URI, in order to avoid 420 | duplicate slashes introduced due to URI placeholders. 421 | ([#48](https://github.com/clue/php-buzz-react/pull/48)) 422 | 423 | ```php 424 | // now correctly returns "http://example.com/path" 425 | // instead of previous "http://example.com//path" 426 | $browser = $browser->withBase('http://example.com/'); 427 | echo $browser->resolve('{+path}', array('path' => '/path')); 428 | 429 | // now correctly returns "http://example.com/path?q=test" 430 | // instead of previous "http://example.com/path/?q=test" 431 | $browser = $browser->withBase('http://example.com/path'); 432 | echo $browser->resolve('{?q}', array('q' => 'test')); 433 | ``` 434 | 435 | ## 0.4.0 (2015-08-09) 436 | 437 | * Feature: Resolve relative URIs, add withBase() and resolve() 438 | ([#41](https://github.com/clue/php-buzz-react/pull/41), [#44](https://github.com/clue/php-buzz-react/pull/44)) 439 | 440 | ```php 441 | $browser = $browser->withBase('http://example.com/'); 442 | $browser->post('/'); 443 | ``` 444 | 445 | * Feature: Resolve URI template placeholders according to RFC 6570 446 | ([#42](https://github.com/clue/php-buzz-react/pull/42), [#44](https://github.com/clue/php-buzz-react/pull/44)) 447 | 448 | ```php 449 | $browser->post($browser->resolve('/{+path}{?version}', array( 450 | 'path' => 'demo.json', 451 | 'version' => '4' 452 | ))); 453 | ``` 454 | 455 | * Feature: Resolve and follow redirects to relative URIs 456 | ([#45](https://github.com/clue/php-buzz-react/pull/45)) 457 | 458 | * Feature / BC break: Simplify Request and Response objects. 459 | Remove Browser::request(), use Browser::send() instead. 460 | ([#37](https://github.com/clue/php-buzz-react/pull/37)) 461 | 462 | ```php 463 | // old 464 | $browser->request('GET', 'http://www.example.com/'); 465 | 466 | // new 467 | $browser->send(new Request('GET', 'http://www.example.com/')); 468 | ``` 469 | 470 | * Feature / Bc break: Enforce absolute URIs via new Uri class 471 | ([#40](https://github.com/clue/php-buzz-react/pull/40), [#44](https://github.com/clue/php-buzz-react/pull/44)) 472 | 473 | * Feature: Add Browser::withSender() method 474 | ([#38](https://github.com/clue/php-buzz-react/pull/38)) 475 | 476 | * Feature: Add Sender::createFromLoopDns() function 477 | ([#39](https://github.com/clue/php-buzz-react/pull/39)) 478 | 479 | * Improve documentation and test suite 480 | 481 | ## 0.3.0 (2015-06-14) 482 | 483 | * Feature: Expose Response object in case of HTTP errors 484 | ([#35](https://github.com/clue/php-buzz-react/pull/35)) 485 | 486 | * Feature: Add experimental `Transaction` options via `Browser` 487 | ([#25](https://github.com/clue/php-buzz-react/pull/25)) 488 | 489 | * Feature: Add experimental streaming API 490 | ([#31](https://github.com/clue/php-buzz-react/pull/31)) 491 | 492 | * Feature: Automatically assign a "Content-Length" header for outgoing `Request`s 493 | ([#29](https://github.com/clue/php-buzz-react/pull/29)) 494 | 495 | * Feature: Add `Message::getHeader()`, it is now available on both `Request` and `Response` 496 | ([#28](https://github.com/clue/php-buzz-react/pull/28)) 497 | 498 | ## 0.2.0 (2014-11-30) 499 | 500 | * Feature: Support communication via UNIX domain sockets 501 | ([#20](https://github.com/clue/php-buzz-react/pull/20)) 502 | 503 | * Fix: Detect immediately failing connection attempt 504 | ([#19](https://github.com/clue/php-buzz-react/issues/19)) 505 | 506 | ## 0.1.2 (2014-10-28) 507 | 508 | * Fix: Strict warning when accessing a single header value 509 | ([#18](https://github.com/clue/php-buzz-react/pull/18) by @masakielastic) 510 | 511 | ## 0.1.1 (2014-05-31) 512 | 513 | * Compatibility with React PHP v0.4 (compatibility with v0.3 preserved) 514 | ([#11](https://github.com/clue/reactphp-buzz/pull/11)) 515 | 516 | ## 0.1.0 (2014-05-27) 517 | 518 | * First tagged release 519 | 520 | ## 0.0.0 (2013-09-01) 521 | 522 | * Initial concept 523 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Christian Lück 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | 3 | This package has now been migrated over to 4 | [react/http](https://github.com/reactphp/http) 5 | and only exists for BC reasons. 6 | 7 | ```bash 8 | $ composer require react/http 9 | ``` 10 | 11 | If you've previously used this package, upgrading should take no longer than a few minutes. 12 | All classes have been merged as-is from the latest `v2.9.0` release with no other significant changes, 13 | so you can simply update your code to use the updated namespace like this: 14 | 15 | ```php 16 | // old 17 | $browser = new Clue\React\Buzz\Browser($loop); 18 | $browser->get($url); 19 | 20 | // new 21 | $browser = new React\Http\Browser($loop); 22 | $browser->get($url); 23 | ``` 24 | 25 | See [react/http](https://github.com/reactphp/http#client-usage) for more details. 26 | 27 | The below documentation applies to the last release of this package. 28 | Further development will take place in the updated 29 | [react/http](https://github.com/reactphp/http), 30 | so you're highly recommended to upgrade as soon as possible. 31 | 32 | # Legacy clue/reactphp-buzz [![Build Status](https://travis-ci.org/clue/reactphp-buzz.svg?branch=master)](https://travis-ci.org/clue/reactphp-buzz) 33 | 34 | Simple, async PSR-7 HTTP client for concurrently processing any number of HTTP requests, 35 | built on top of [ReactPHP](https://reactphp.org/). 36 | 37 | This library is heavily inspired by the great 38 | [kriswallsmith/Buzz](https://github.com/kriswallsmith/Buzz) 39 | project. However, instead of blocking on each request, it relies on 40 | [ReactPHP's EventLoop](https://github.com/reactphp/event-loop) to process 41 | multiple requests in parallel. 42 | This allows you to interact with multiple HTTP servers 43 | (fetch URLs, talk to RESTful APIs, follow redirects etc.) 44 | at the same time. 45 | Unlike the underlying [react/http-client](https://github.com/reactphp/http-client), 46 | this project aims at providing a higher-level API that is easy to use 47 | in order to process multiple HTTP requests concurrently without having to 48 | mess with most of the low-level details. 49 | 50 | * **Async execution of HTTP requests** - 51 | Send any number of HTTP requests to any number of HTTP servers in parallel and 52 | process their responses as soon as results come in. 53 | The Promise-based design provides a *sane* interface to working with out of bound responses. 54 | * **Standard interfaces** - 55 | Allows easy integration with existing higher-level components by implementing 56 | [PSR-7 (http-message)](https://www.php-fig.org/psr/psr-7/) interfaces, 57 | ReactPHP's standard [promises](#promises) and [streaming interfaces](#streaming-response). 58 | * **Lightweight, SOLID design** - 59 | Provides a thin abstraction that is [*just good enough*](https://en.wikipedia.org/wiki/Principle_of_good_enough) 60 | and does not get in your way. 61 | Builds on top of well-tested components and well-established concepts instead of reinventing the wheel. 62 | * **Good test coverage** - 63 | Comes with an automated tests suite and is regularly tested in the *real world*. 64 | 65 | **Table of contents** 66 | 67 | * [Support us](#support-us) 68 | * [Quickstart example](#quickstart-example) 69 | * [Usage](#usage) 70 | * [Request methods](#request-methods) 71 | * [Promises](#promises) 72 | * [Cancellation](#cancellation) 73 | * [Timeouts](#timeouts) 74 | * [Authentication](#authentication) 75 | * [Redirects](#redirects) 76 | * [Blocking](#blocking) 77 | * [Concurrency](#concurrency) 78 | * [Streaming response](#streaming-response) 79 | * [Streaming request](#streaming-request) 80 | * [HTTP proxy](#http-proxy) 81 | * [SOCKS proxy](#socks-proxy) 82 | * [SSH proxy](#ssh-proxy) 83 | * [Unix domain sockets](#unix-domain-sockets) 84 | * [API](#api) 85 | * [Browser](#browser) 86 | * [get()](#get) 87 | * [post()](#post) 88 | * [head()](#head) 89 | * [patch()](#patch) 90 | * [put()](#put) 91 | * [delete()](#delete) 92 | * [request()](#request) 93 | * [requestStreaming()](#requeststreaming) 94 | * [~~submit()~~](#submit) 95 | * [~~send()~~](#send) 96 | * [withTimeout()](#withtimeout) 97 | * [withFollowRedirects()](#withfollowredirects) 98 | * [withRejectErrorResponse()](#withrejecterrorresponse) 99 | * [withBase()](#withbase) 100 | * [withProtocolVersion()](#withprotocolversion) 101 | * [withResponseBuffer()](#withresponsebuffer) 102 | * [~~withOptions()~~](#withoptions) 103 | * [~~withoutBase()~~](#withoutbase) 104 | * [ResponseInterface](#responseinterface) 105 | * [RequestInterface](#requestinterface) 106 | * [UriInterface](#uriinterface) 107 | * [ResponseException](#responseexception) 108 | * [Install](#install) 109 | * [Tests](#tests) 110 | * [License](#license) 111 | 112 | ## Support us 113 | 114 | We invest a lot of time developing, maintaining and updating our awesome 115 | open-source projects. You can help us sustain this high-quality of our work by 116 | [becoming a sponsor on GitHub](https://github.com/sponsors/clue). Sponsors get 117 | numerous benefits in return, see our [sponsoring page](https://github.com/sponsors/clue) 118 | for details. 119 | 120 | Let's take these projects to the next level together! 🚀 121 | 122 | ## Quickstart example 123 | 124 | Once [installed](#install), you can use the following code to access a 125 | HTTP webserver and send some simple HTTP GET requests: 126 | 127 | ```php 128 | $loop = React\EventLoop\Factory::create(); 129 | $client = new Clue\React\Buzz\Browser($loop); 130 | 131 | $client->get('http://www.google.com/')->then(function (Psr\Http\Message\ResponseInterface $response) { 132 | var_dump($response->getHeaders(), (string)$response->getBody()); 133 | }); 134 | 135 | $loop->run(); 136 | ``` 137 | 138 | See also the [examples](examples). 139 | 140 | ## Usage 141 | 142 | ### Request methods 143 | 144 | 145 | 146 | Most importantly, this project provides a [`Browser`](#browser) object that 147 | offers several methods that resemble the HTTP protocol methods: 148 | 149 | ```php 150 | $browser->get($url, array $headers = array()); 151 | $browser->head($url, array $headers = array()); 152 | $browser->post($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); 153 | $browser->delete($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); 154 | $browser->put($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); 155 | $browser->patch($url, array $headers = array(), string|ReadableStreamInterface $contents = ''); 156 | ``` 157 | 158 | Each of these methods requires a `$url` and some optional parameters to send an 159 | HTTP request. Each of these method names matches the respective HTTP request 160 | method, for example the [`get()`](#get) method sends an HTTP `GET` request. 161 | 162 | You can optionally pass an associative array of additional `$headers` that will be 163 | sent with this HTTP request. Additionally, each method will automatically add a 164 | matching `Content-Length` request header if an outgoing request body is given and its 165 | size is known and non-empty. For an empty request body, if will only include a 166 | `Content-Length: 0` request header if the request method usually expects a request 167 | body (only applies to `POST`, `PUT` and `PATCH` HTTP request methods). 168 | 169 | If you're using a [streaming request body](#streaming-request), it will default 170 | to using `Transfer-Encoding: chunked` unless you explicitly pass in a matching `Content-Length` 171 | request header. See also [streaming request](#streaming-request) for more details. 172 | 173 | By default, all of the above methods default to sending requests using the 174 | HTTP/1.1 protocol version. If you want to explicitly use the legacy HTTP/1.0 175 | protocol version, you can use the [`withProtocolVersion()`](#withprotocolversion) 176 | method. If you want to use any other or even custom HTTP request method, you can 177 | use the [`request()`](#request) method. 178 | 179 | Each of the above methods supports async operation and either *fulfills* with a 180 | [`ResponseInterface`](#responseinterface) or *rejects* with an `Exception`. 181 | Please see the following chapter about [promises](#promises) for more details. 182 | 183 | ### Promises 184 | 185 | Sending requests is async (non-blocking), so you can actually send multiple 186 | requests in parallel. 187 | The `Browser` will respond to each request with a [`ResponseInterface`](#responseinterface) 188 | message, the order is not guaranteed. 189 | Sending requests uses a [Promise](https://github.com/reactphp/promise)-based 190 | interface that makes it easy to react to when an HTTP request is completed 191 | (i.e. either successfully fulfilled or rejected with an error): 192 | 193 | ```php 194 | $browser->get($url)->then( 195 | function (Psr\Http\Message\ResponseInterface $response) { 196 | var_dump('Response received', $response); 197 | }, 198 | function (Exception $error) { 199 | var_dump('There was an error', $error->getMessage()); 200 | } 201 | ); 202 | ``` 203 | 204 | If this looks strange to you, you can also use the more traditional [blocking API](#blocking). 205 | 206 | Keep in mind that resolving the Promise with the full response message means the 207 | whole response body has to be kept in memory. 208 | This is easy to get started and works reasonably well for smaller responses 209 | (such as common HTML pages or RESTful or JSON API requests). 210 | 211 | You may also want to look into the [streaming API](#streaming-response): 212 | 213 | * If you're dealing with lots of concurrent requests (100+) or 214 | * If you want to process individual data chunks as they happen (without having to wait for the full response body) or 215 | * If you're expecting a big response body size (1 MiB or more, for example when downloading binary files) or 216 | * If you're unsure about the response body size (better be safe than sorry when accessing arbitrary remote HTTP endpoints and the response body size is unknown in advance). 217 | 218 | ### Cancellation 219 | 220 | The returned Promise is implemented in such a way that it can be cancelled 221 | when it is still pending. 222 | Cancelling a pending promise will reject its value with an Exception and 223 | clean up any underlying resources. 224 | 225 | ```php 226 | $promise = $browser->get($url); 227 | 228 | $loop->addTimer(2.0, function () use ($promise) { 229 | $promise->cancel(); 230 | }); 231 | ``` 232 | 233 | ### Timeouts 234 | 235 | This library uses a very efficient HTTP implementation, so most HTTP requests 236 | should usually be completed in mere milliseconds. However, when sending HTTP 237 | requests over an unreliable network (the internet), there are a number of things 238 | that can go wrong and may cause the request to fail after a time. As such, this 239 | library respects PHP's `default_socket_timeout` setting (default 60s) as a timeout 240 | for sending the outgoing HTTP request and waiting for a successful response and 241 | will otherwise cancel the pending request and reject its value with an Exception. 242 | 243 | Note that this timeout value covers creating the underlying transport connection, 244 | sending the HTTP request, receiving the HTTP response headers and its full 245 | response body and following any eventual [redirects](#redirects). See also 246 | [redirects](#redirects) below to configure the number of redirects to follow (or 247 | disable following redirects altogether) and also [streaming](#streaming-response) 248 | below to not take receiving large response bodies into account for this timeout. 249 | 250 | You can use the [`withTimeout()` method](#withtimeout) to pass a custom timeout 251 | value in seconds like this: 252 | 253 | ```php 254 | $browser = $browser->withTimeout(10.0); 255 | 256 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 257 | // response received within 10 seconds maximum 258 | var_dump($response->getHeaders()); 259 | }); 260 | ``` 261 | 262 | Similarly, you can use a bool `false` to not apply a timeout at all 263 | or use a bool `true` value to restore the default handling. 264 | See [`withTimeout()`](#withtimeout) for more details. 265 | 266 | If you're using a [streaming response body](#streaming-response), the time it 267 | takes to receive the response body stream will not be included in the timeout. 268 | This allows you to keep this incoming stream open for a longer time, such as 269 | when downloading a very large stream or when streaming data over a long-lived 270 | connection. 271 | 272 | If you're using a [streaming request body](#streaming-request), the time it 273 | takes to send the request body stream will not be included in the timeout. This 274 | allows you to keep this outgoing stream open for a longer time, such as when 275 | uploading a very large stream. 276 | 277 | Note that this timeout handling applies to the higher-level HTTP layer. Lower 278 | layers such as socket and DNS may also apply (different) timeout values. In 279 | particular, the underlying socket connection uses the same `default_socket_timeout` 280 | setting to establish the underlying transport connection. To control this 281 | connection timeout behavior, you can [inject a custom `Connector`](#browser) 282 | like this: 283 | 284 | ```php 285 | $browser = new Clue\React\Buzz\Browser( 286 | $loop, 287 | new React\Socket\Connector( 288 | $loop, 289 | array( 290 | 'timeout' => 5 291 | ) 292 | ) 293 | ); 294 | ``` 295 | 296 | ### Authentication 297 | 298 | This library supports [HTTP Basic Authentication](https://en.wikipedia.org/wiki/Basic_access_authentication) 299 | using the `Authorization: Basic …` request header or allows you to set an explicit 300 | `Authorization` request header. 301 | 302 | By default, this library does not include an outgoing `Authorization` request 303 | header. If the server requires authentication, if may return a `401` (Unauthorized) 304 | status code which will reject the request by default (see also the 305 | [`withRejectErrorResponse()` method](#withrejecterrorresponse) below). 306 | 307 | In order to pass authentication details, you can simple pass the username and 308 | password as part of the request URL like this: 309 | 310 | ```php 311 | $promise = $browser->get('https://user:pass@example.com/api'); 312 | ``` 313 | 314 | Note that special characters in the authentication details have to be 315 | percent-encoded, see also [`rawurlencode()`](https://www.php.net/manual/en/function.rawurlencode.php). 316 | This example will automatically pass the base64-encoded authentication details 317 | using the outgoing `Authorization: Basic …` request header. If the HTTP endpoint 318 | you're talking to requires any other authentication scheme, you can also pass 319 | this header explicitly. This is common when using (RESTful) HTTP APIs that use 320 | OAuth access tokens or JSON Web Tokens (JWT): 321 | 322 | ```php 323 | $token = 'abc123'; 324 | 325 | $promise = $browser->get( 326 | 'https://example.com/api', 327 | array( 328 | 'Authorization' => 'Bearer ' . $token 329 | ) 330 | ); 331 | ``` 332 | 333 | When following redirects, the `Authorization` request header will never be sent 334 | to any remote hosts by default. When following a redirect where the `Location` 335 | response header contains authentication details, these details will be sent for 336 | following requests. See also [redirects](#redirects) below. 337 | 338 | ### Redirects 339 | 340 | By default, this library follows any redirects and obeys `3xx` (Redirection) 341 | status codes using the `Location` response header from the remote server. 342 | The promise will be fulfilled with the last response from the chain of redirects. 343 | 344 | ```php 345 | $browser->get($url, $headers)->then(function (Psr\Http\Message\ResponseInterface $response) { 346 | // the final response will end up here 347 | var_dump($response->getHeaders()); 348 | }); 349 | ``` 350 | 351 | Any redirected requests will follow the semantics of the original request and 352 | will include the same request headers as the original request except for those 353 | listed below. 354 | If the original request contained a request body, this request body will never 355 | be passed to the redirected request. Accordingly, each redirected request will 356 | remove any `Content-Length` and `Content-Type` request headers. 357 | 358 | If the original request used HTTP authentication with an `Authorization` request 359 | header, this request header will only be passed as part of the redirected 360 | request if the redirected URL is using the same host. In other words, the 361 | `Authorizaton` request header will not be forwarded to other foreign hosts due to 362 | possible privacy/security concerns. When following a redirect where the `Location` 363 | response header contains authentication details, these details will be sent for 364 | following requests. 365 | 366 | You can use the [`withFollowRedirects()`](#withfollowredirects) method to 367 | control the maximum number of redirects to follow or to return any redirect 368 | responses as-is and apply custom redirection logic like this: 369 | 370 | ```php 371 | $browser = $browser->withFollowRedirects(false); 372 | 373 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 374 | // any redirects will now end up here 375 | var_dump($response->getHeaders()); 376 | }); 377 | ``` 378 | 379 | See also [`withFollowRedirects()`](#withfollowredirects) for more details. 380 | 381 | ### Blocking 382 | 383 | As stated above, this library provides you a powerful, async API by default. 384 | 385 | If, however, you want to integrate this into your traditional, blocking environment, 386 | you should look into also using [clue/reactphp-block](https://github.com/clue/reactphp-block). 387 | 388 | The resulting blocking code could look something like this: 389 | 390 | ```php 391 | use Clue\React\Block; 392 | 393 | $loop = React\EventLoop\Factory::create(); 394 | $browser = new Clue\React\Buzz\Browser($loop); 395 | 396 | $promise = $browser->get('http://example.com/'); 397 | 398 | try { 399 | $response = Block\await($promise, $loop); 400 | // response successfully received 401 | } catch (Exception $e) { 402 | // an error occured while performing the request 403 | } 404 | ``` 405 | 406 | Similarly, you can also process multiple requests concurrently and await an array of `Response` objects: 407 | 408 | ```php 409 | $promises = array( 410 | $browser->get('http://example.com/'), 411 | $browser->get('http://www.example.org/'), 412 | ); 413 | 414 | $responses = Block\awaitAll($promises, $loop); 415 | ``` 416 | 417 | Please refer to [clue/reactphp-block](https://github.com/clue/reactphp-block#readme) for more details. 418 | 419 | Keep in mind the above remark about buffering the whole response message in memory. 420 | As an alternative, you may also see one of the following chapters for the 421 | [streaming API](#streaming-response). 422 | 423 | ### Concurrency 424 | 425 | As stated above, this library provides you a powerful, async API. Being able to 426 | send a large number of requests at once is one of the core features of this 427 | project. For instance, you can easily send 100 requests concurrently while 428 | processing SQL queries at the same time. 429 | 430 | Remember, with great power comes great responsibility. Sending an excessive 431 | number of requests may either take up all resources on your side or it may even 432 | get you banned by the remote side if it sees an unreasonable number of requests 433 | from your side. 434 | 435 | ```php 436 | // watch out if array contains many elements 437 | foreach ($urls as $url) { 438 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 439 | var_dump($response->getHeaders()); 440 | }); 441 | } 442 | ``` 443 | 444 | As a consequence, it's usually recommended to limit concurrency on the sending 445 | side to a reasonable value. It's common to use a rather small limit, as doing 446 | more than a dozen of things at once may easily overwhelm the receiving side. You 447 | can use [clue/reactphp-mq](https://github.com/clue/reactphp-mq) as a lightweight 448 | in-memory queue to concurrently do many (but not too many) things at once: 449 | 450 | ```php 451 | // wraps Browser in a Queue object that executes no more than 10 operations at once 452 | $q = new Clue\React\Mq\Queue(10, null, function ($url) use ($browser) { 453 | return $browser->get($url); 454 | }); 455 | 456 | foreach ($urls as $url) { 457 | $q($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 458 | var_dump($response->getHeaders()); 459 | }); 460 | } 461 | ``` 462 | 463 | Additional requests that exceed the concurrency limit will automatically be 464 | enqueued until one of the pending requests completes. This integrates nicely 465 | with the existing [Promise-based API](#promises). Please refer to 466 | [clue/reactphp-mq](https://github.com/clue/reactphp-mq) for more details. 467 | 468 | This in-memory approach works reasonably well for some thousand outstanding 469 | requests. If you're processing a very large input list (think millions of rows 470 | in a CSV or NDJSON file), you may want to look into using a streaming approach 471 | instead. See [clue/reactphp-flux](https://github.com/clue/reactphp-flux) for 472 | more details. 473 | 474 | ### Streaming response 475 | 476 | 477 | 478 | All of the above examples assume you want to store the whole response body in memory. 479 | This is easy to get started and works reasonably well for smaller responses. 480 | 481 | However, there are several situations where it's usually a better idea to use a 482 | streaming approach, where only small chunks have to be kept in memory: 483 | 484 | * If you're dealing with lots of concurrent requests (100+) or 485 | * If you want to process individual data chunks as they happen (without having to wait for the full response body) or 486 | * If you're expecting a big response body size (1 MiB or more, for example when downloading binary files) or 487 | * If you're unsure about the response body size (better be safe than sorry when accessing arbitrary remote HTTP endpoints and the response body size is unknown in advance). 488 | 489 | You can use the [`requestStreaming()`](#requeststreaming) method to send an 490 | arbitrary HTTP request and receive a streaming response. It uses the same HTTP 491 | message API, but does not buffer the response body in memory. It only processes 492 | the response body in small chunks as data is received and forwards this data 493 | through [ReactPHP's Stream API](https://github.com/reactphp/stream). This works 494 | for (any number of) responses of arbitrary sizes. 495 | 496 | This means it resolves with a normal [`ResponseInterface`](#responseinterface), 497 | which can be used to access the response message parameters as usual. 498 | You can access the message body as usual, however it now also 499 | implements ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) 500 | as well as parts of the PSR-7's [`StreamInterface`](https://www.php-fig.org/psr/psr-7/#3-4-psr-http-message-streaminterface). 501 | 502 | ```php 503 | $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { 504 | $body = $response->getBody(); 505 | assert($body instanceof Psr\Http\Message\StreamInterface); 506 | assert($body instanceof React\Stream\ReadableStreamInterface); 507 | 508 | $body->on('data', function ($chunk) { 509 | echo $chunk; 510 | }); 511 | 512 | $body->on('error', function (Exception $error) { 513 | echo 'Error: ' . $error->getMessage() . PHP_EOL; 514 | }); 515 | 516 | $body->on('close', function () { 517 | echo '[DONE]' . PHP_EOL; 518 | }); 519 | }); 520 | ``` 521 | 522 | See also the [stream download example](examples/91-benchmark-download.php) and 523 | the [stream forwarding example](examples/21-stream-forwarding.php). 524 | 525 | You can invoke the following methods on the message body: 526 | 527 | ```php 528 | $body->on($event, $callback); 529 | $body->eof(); 530 | $body->isReadable(); 531 | $body->pipe(React\Stream\WritableStreamInterface $dest, array $options = array()); 532 | $body->close(); 533 | $body->pause(); 534 | $body->resume(); 535 | ``` 536 | 537 | Because the message body is in a streaming state, invoking the following methods 538 | doesn't make much sense: 539 | 540 | ```php 541 | $body->__toString(); // '' 542 | $body->detach(); // throws BadMethodCallException 543 | $body->getSize(); // null 544 | $body->tell(); // throws BadMethodCallException 545 | $body->isSeekable(); // false 546 | $body->seek(); // throws BadMethodCallException 547 | $body->rewind(); // throws BadMethodCallException 548 | $body->isWritable(); // false 549 | $body->write(); // throws BadMethodCallException 550 | $body->read(); // throws BadMethodCallException 551 | $body->getContents(); // throws BadMethodCallException 552 | ``` 553 | 554 | Note how [timeouts](#timeouts) apply slightly differently when using streaming. 555 | In streaming mode, the timeout value covers creating the underlying transport 556 | connection, sending the HTTP request, receiving the HTTP response headers and 557 | following any eventual [redirects](#redirects). In particular, the timeout value 558 | does not take receiving (possibly large) response bodies into account. 559 | 560 | If you want to integrate the streaming response into a higher level API, then 561 | working with Promise objects that resolve with Stream objects is often inconvenient. 562 | Consider looking into also using [react/promise-stream](https://github.com/reactphp/promise-stream). 563 | The resulting streaming code could look something like this: 564 | 565 | ```php 566 | use React\Promise\Stream; 567 | 568 | function download(Browser $browser, string $url): React\Stream\ReadableStreamInterface { 569 | return Stream\unwrapReadable( 570 | $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { 571 | return $response->getBody(); 572 | }) 573 | ); 574 | } 575 | 576 | $stream = download($browser, $url); 577 | $stream->on('data', function ($data) { 578 | echo $data; 579 | }); 580 | ``` 581 | 582 | See also the [`requestStreaming()`](#requeststreaming) method for more details. 583 | 584 | > Legacy info: Legacy versions prior to v2.9.0 used the legacy 585 | [`streaming` option](#withoptions). This option is now deprecated but otherwise 586 | continues to show the exact same behavior. 587 | 588 | ### Streaming request 589 | 590 | Besides streaming the response body, you can also stream the request body. 591 | This can be useful if you want to send big POST requests (uploading files etc.) 592 | or process many outgoing streams at once. 593 | Instead of passing the body as a string, you can simply pass an instance 594 | implementing ReactPHP's [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) 595 | to the [request methods](#request-methods) like this: 596 | 597 | ```php 598 | $browser->post($url, array(), $stream)->then(function (Psr\Http\Message\ResponseInterface $response) { 599 | echo 'Successfully sent.'; 600 | }); 601 | ``` 602 | 603 | If you're using a streaming request body (`React\Stream\ReadableStreamInterface`), it will 604 | default to using `Transfer-Encoding: chunked` or you have to explicitly pass in a 605 | matching `Content-Length` request header like so: 606 | 607 | ```php 608 | $body = new React\Stream\ThroughStream(); 609 | $loop->addTimer(1.0, function () use ($body) { 610 | $body->end("hello world"); 611 | }); 612 | 613 | $browser->post($url, array('Content-Length' => '11'), $body); 614 | ``` 615 | 616 | If the streaming request body emits an `error` event or is explicitly closed 617 | without emitting a successful `end` event first, the request will automatically 618 | be closed and rejected. 619 | 620 | ### HTTP proxy 621 | 622 | You can also establish your outgoing connections through an HTTP CONNECT proxy server 623 | by adding a dependency to [clue/reactphp-http-proxy](https://github.com/clue/reactphp-http-proxy). 624 | 625 | HTTP CONNECT proxy servers (also commonly known as "HTTPS proxy" or "SSL proxy") 626 | are commonly used to tunnel HTTPS traffic through an intermediary ("proxy"), to 627 | conceal the origin address (anonymity) or to circumvent address blocking 628 | (geoblocking). While many (public) HTTP CONNECT proxy servers often limit this 629 | to HTTPS port`443` only, this can technically be used to tunnel any TCP/IP-based 630 | protocol, such as plain HTTP and TLS-encrypted HTTPS. 631 | 632 | ```php 633 | $proxy = new Clue\React\HttpProxy\ProxyConnector( 634 | 'http://127.0.0.1:8080', 635 | new React\Socket\Connector($loop) 636 | ); 637 | 638 | $connector = new React\Socket\Connector($loop, array( 639 | 'tcp' => $proxy, 640 | 'dns' => false 641 | )); 642 | 643 | $browser = new Clue\React\Buzz\Browser($loop, $connector); 644 | ``` 645 | 646 | See also the [HTTP CONNECT proxy example](examples/11-http-proxy.php). 647 | 648 | ### SOCKS proxy 649 | 650 | You can also establish your outgoing connections through a SOCKS proxy server 651 | by adding a dependency to [clue/reactphp-socks](https://github.com/clue/reactphp-socks). 652 | 653 | The SOCKS proxy protocol family (SOCKS5, SOCKS4 and SOCKS4a) is commonly used to 654 | tunnel HTTP(S) traffic through an intermediary ("proxy"), to conceal the origin 655 | address (anonymity) or to circumvent address blocking (geoblocking). While many 656 | (public) SOCKS proxy servers often limit this to HTTP(S) port `80` and `443` 657 | only, this can technically be used to tunnel any TCP/IP-based protocol. 658 | 659 | ```php 660 | $proxy = new Clue\React\Socks\Client( 661 | 'socks://127.0.0.1:1080', 662 | new React\Socket\Connector($loop) 663 | ); 664 | 665 | $connector = new React\Socket\Connector($loop, array( 666 | 'tcp' => $proxy, 667 | 'dns' => false 668 | )); 669 | 670 | $browser = new Clue\React\Buzz\Browser($loop, $connector); 671 | ``` 672 | 673 | See also the [SOCKS proxy example](examples/12-socks-proxy.php). 674 | 675 | ### SSH proxy 676 | 677 | You can also establish your outgoing connections through an SSH server 678 | by adding a dependency to [clue/reactphp-ssh-proxy](https://github.com/clue/reactphp-ssh-proxy). 679 | 680 | [Secure Shell (SSH)](https://en.wikipedia.org/wiki/Secure_Shell) is a secure 681 | network protocol that is most commonly used to access a login shell on a remote 682 | server. Its architecture allows it to use multiple secure channels over a single 683 | connection. Among others, this can also be used to create an "SSH tunnel", which 684 | is commonly used to tunnel HTTP(S) traffic through an intermediary ("proxy"), to 685 | conceal the origin address (anonymity) or to circumvent address blocking 686 | (geoblocking). This can be used to tunnel any TCP/IP-based protocol (HTTP, SMTP, 687 | IMAP etc.), allows you to access local services that are otherwise not accessible 688 | from the outside (database behind firewall) and as such can also be used for 689 | plain HTTP and TLS-encrypted HTTPS. 690 | 691 | ```php 692 | $proxy = new Clue\React\SshProxy\SshSocksConnector('me@localhost:22', $loop); 693 | 694 | $connector = new React\Socket\Connector($loop, array( 695 | 'tcp' => $proxy, 696 | 'dns' => false 697 | )); 698 | 699 | $browser = new Clue\React\Buzz\Browser($loop, $connector); 700 | ``` 701 | 702 | See also the [SSH proxy example](examples/13-ssh-proxy.php). 703 | 704 | ### Unix domain sockets 705 | 706 | By default, this library supports transport over plaintext TCP/IP and secure 707 | TLS connections for the `http://` and `https://` URL schemes respectively. 708 | This library also supports Unix domain sockets (UDS) when explicitly configured. 709 | 710 | In order to use a UDS path, you have to explicitly configure the connector to 711 | override the destination URL so that the hostname given in the request URL will 712 | no longer be used to establish the connection: 713 | 714 | ```php 715 | $connector = new React\Socket\FixedUriConnector( 716 | 'unix:///var/run/docker.sock', 717 | new React\Socket\UnixConnector($loop) 718 | ); 719 | 720 | $browser = new Browser($loop, $connector); 721 | 722 | $client->get('http://localhost/info')->then(function (Psr\Http\Message\ResponseInterface $response) { 723 | var_dump($response->getHeaders(), (string)$response->getBody()); 724 | }); 725 | ``` 726 | 727 | See also the [Unix Domain Sockets (UDS) example](examples/14-unix-domain-sockets.php). 728 | 729 | ## API 730 | 731 | ### Browser 732 | 733 | The `Clue\React\Buzz\Browser` is responsible for sending HTTP requests to your HTTP server 734 | and keeps track of pending incoming HTTP responses. 735 | It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage). 736 | 737 | ```php 738 | $loop = React\EventLoop\Factory::create(); 739 | 740 | $browser = new Clue\React\Buzz\Browser($loop); 741 | ``` 742 | 743 | If you need custom connector settings (DNS resolution, TLS parameters, timeouts, 744 | proxy servers etc.), you can explicitly pass a custom instance of the 745 | [`ConnectorInterface`](https://github.com/reactphp/socket#connectorinterface): 746 | 747 | ```php 748 | $connector = new React\Socket\Connector($loop, array( 749 | 'dns' => '127.0.0.1', 750 | 'tcp' => array( 751 | 'bindto' => '192.168.10.1:0' 752 | ), 753 | 'tls' => array( 754 | 'verify_peer' => false, 755 | 'verify_peer_name' => false 756 | ) 757 | )); 758 | 759 | $browser = new Clue\React\Buzz\Browser($loop, $connector); 760 | ``` 761 | 762 | #### get() 763 | 764 | The `get(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to 765 | send an HTTP GET request. 766 | 767 | ```php 768 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 769 | var_dump((string)$response->getBody()); 770 | }); 771 | ``` 772 | 773 | See also [example 01](examples/01-google.php). 774 | 775 | > For BC reasons, this method accepts the `$url` as either a `string` 776 | value or as an `UriInterface`. It's recommended to explicitly cast any 777 | objects implementing `UriInterface` to `string`. 778 | 779 | #### post() 780 | 781 | The `post(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to 782 | send an HTTP POST request. 783 | 784 | ```php 785 | $browser->post( 786 | $url, 787 | [ 788 | 'Content-Type' => 'application/json' 789 | ], 790 | json_encode($data) 791 | )->then(function (Psr\Http\Message\ResponseInterface $response) { 792 | var_dump(json_decode((string)$response->getBody())); 793 | }); 794 | ``` 795 | 796 | See also [example 04](examples/04-post-json.php). 797 | 798 | This method is also commonly used to submit HTML form data: 799 | 800 | ```php 801 | $data = [ 802 | 'user' => 'Alice', 803 | 'password' => 'secret' 804 | ]; 805 | 806 | $browser->post( 807 | $url, 808 | [ 809 | 'Content-Type' => 'application/x-www-form-urlencoded' 810 | ], 811 | http_build_query($data) 812 | ); 813 | ``` 814 | 815 | This method will automatically add a matching `Content-Length` request 816 | header if the outgoing request body is a `string`. If you're using a 817 | streaming request body (`ReadableStreamInterface`), it will default to 818 | using `Transfer-Encoding: chunked` or you have to explicitly pass in a 819 | matching `Content-Length` request header like so: 820 | 821 | ```php 822 | $body = new React\Stream\ThroughStream(); 823 | $loop->addTimer(1.0, function () use ($body) { 824 | $body->end("hello world"); 825 | }); 826 | 827 | $browser->post($url, array('Content-Length' => '11'), $body); 828 | ``` 829 | 830 | > For BC reasons, this method accepts the `$url` as either a `string` 831 | value or as an `UriInterface`. It's recommended to explicitly cast any 832 | objects implementing `UriInterface` to `string`. 833 | 834 | #### head() 835 | 836 | The `head(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to 837 | send an HTTP HEAD request. 838 | 839 | ```php 840 | $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 841 | var_dump($response->getHeaders()); 842 | }); 843 | ``` 844 | 845 | > For BC reasons, this method accepts the `$url` as either a `string` 846 | value or as an `UriInterface`. It's recommended to explicitly cast any 847 | objects implementing `UriInterface` to `string`. 848 | 849 | #### patch() 850 | 851 | The `patch(string|UriInterface $url, array $headers = array(), string|ReadableStreamInterface $contents = ''): PromiseInterface` method can be used to 852 | send an HTTP PATCH request. 853 | 854 | ```php 855 | $browser->patch( 856 | $url, 857 | [ 858 | 'Content-Type' => 'application/json' 859 | ], 860 | json_encode($data) 861 | )->then(function (Psr\Http\Message\ResponseInterface $response) { 862 | var_dump(json_decode((string)$response->getBody())); 863 | }); 864 | ``` 865 | 866 | This method will automatically add a matching `Content-Length` request 867 | header if the outgoing request body is a `string`. If you're using a 868 | streaming request body (`ReadableStreamInterface`), it will default to 869 | using `Transfer-Encoding: chunked` or you have to explicitly pass in a 870 | matching `Content-Length` request header like so: 871 | 872 | ```php 873 | $body = new React\Stream\ThroughStream(); 874 | $loop->addTimer(1.0, function () use ($body) { 875 | $body->end("hello world"); 876 | }); 877 | 878 | $browser->patch($url, array('Content-Length' => '11'), $body); 879 | ``` 880 | 881 | > For BC reasons, this method accepts the `$url` as either a `string` 882 | value or as an `UriInterface`. It's recommended to explicitly cast any 883 | objects implementing `UriInterface` to `string`. 884 | 885 | #### put() 886 | 887 | The `put(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to 888 | send an HTTP PUT request. 889 | 890 | ```php 891 | $browser->put( 892 | $url, 893 | [ 894 | 'Content-Type' => 'text/xml' 895 | ], 896 | $xml->asXML() 897 | )->then(function (Psr\Http\Message\ResponseInterface $response) { 898 | var_dump((string)$response->getBody()); 899 | }); 900 | ``` 901 | 902 | See also [example 05](examples/05-put-xml.php). 903 | 904 | This method will automatically add a matching `Content-Length` request 905 | header if the outgoing request body is a `string`. If you're using a 906 | streaming request body (`ReadableStreamInterface`), it will default to 907 | using `Transfer-Encoding: chunked` or you have to explicitly pass in a 908 | matching `Content-Length` request header like so: 909 | 910 | ```php 911 | $body = new React\Stream\ThroughStream(); 912 | $loop->addTimer(1.0, function () use ($body) { 913 | $body->end("hello world"); 914 | }); 915 | 916 | $browser->put($url, array('Content-Length' => '11'), $body); 917 | ``` 918 | 919 | > For BC reasons, this method accepts the `$url` as either a `string` 920 | value or as an `UriInterface`. It's recommended to explicitly cast any 921 | objects implementing `UriInterface` to `string`. 922 | 923 | #### delete() 924 | 925 | The `delete(string|UriInterface $url, array $headers = array()): PromiseInterface` method can be used to 926 | send an HTTP DELETE request. 927 | 928 | ```php 929 | $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 930 | var_dump((string)$response->getBody()); 931 | }); 932 | ``` 933 | 934 | > For BC reasons, this method accepts the `$url` as either a `string` 935 | value or as an `UriInterface`. It's recommended to explicitly cast any 936 | objects implementing `UriInterface` to `string`. 937 | 938 | #### request() 939 | 940 | The `request(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to 941 | send an arbitrary HTTP request. 942 | 943 | The preferred way to send an HTTP request is by using the above 944 | [request methods](#request-methods), for example the [`get()`](#get) 945 | method to send an HTTP `GET` request. 946 | 947 | As an alternative, if you want to use a custom HTTP request method, you 948 | can use this method: 949 | 950 | ```php 951 | $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { 952 | var_dump((string)$response->getBody()); 953 | }); 954 | ``` 955 | 956 | This method will automatically add a matching `Content-Length` request 957 | header if the size of the outgoing request body is known and non-empty. 958 | For an empty request body, if will only include a `Content-Length: 0` 959 | request header if the request method usually expects a request body (only 960 | applies to `POST`, `PUT` and `PATCH`). 961 | 962 | If you're using a streaming request body (`ReadableStreamInterface`), it 963 | will default to using `Transfer-Encoding: chunked` or you have to 964 | explicitly pass in a matching `Content-Length` request header like so: 965 | 966 | ```php 967 | $body = new React\Stream\ThroughStream(); 968 | $loop->addTimer(1.0, function () use ($body) { 969 | $body->end("hello world"); 970 | }); 971 | 972 | $browser->request('POST', $url, array('Content-Length' => '11'), $body); 973 | ``` 974 | 975 | > Note that this method is available as of v2.9.0 and always buffers the 976 | response body before resolving. 977 | It does not respect the deprecated [`streaming` option](#withoptions). 978 | If you want to stream the response body, you can use the 979 | [`requestStreaming()`](#requeststreaming) method instead. 980 | 981 | #### requestStreaming() 982 | 983 | The `requestStreaming(string $method, string $url, array $headers = array(), string|ReadableStreamInterface $body = ''): PromiseInterface` method can be used to 984 | send an arbitrary HTTP request and receive a streaming response without buffering the response body. 985 | 986 | The preferred way to send an HTTP request is by using the above 987 | [request methods](#request-methods), for example the [`get()`](#get) 988 | method to send an HTTP `GET` request. Each of these methods will buffer 989 | the whole response body in memory by default. This is easy to get started 990 | and works reasonably well for smaller responses. 991 | 992 | In some situations, it's a better idea to use a streaming approach, where 993 | only small chunks have to be kept in memory. You can use this method to 994 | send an arbitrary HTTP request and receive a streaming response. It uses 995 | the same HTTP message API, but does not buffer the response body in 996 | memory. It only processes the response body in small chunks as data is 997 | received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). 998 | This works for (any number of) responses of arbitrary sizes. 999 | 1000 | ```php 1001 | $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { 1002 | $body = $response->getBody(); 1003 | assert($body instanceof Psr\Http\Message\StreamInterface); 1004 | assert($body instanceof React\Stream\ReadableStreamInterface); 1005 | 1006 | $body->on('data', function ($chunk) { 1007 | echo $chunk; 1008 | }); 1009 | 1010 | $body->on('error', function (Exception $error) { 1011 | echo 'Error: ' . $error->getMessage() . PHP_EOL; 1012 | }); 1013 | 1014 | $body->on('close', function () { 1015 | echo '[DONE]' . PHP_EOL; 1016 | }); 1017 | }); 1018 | ``` 1019 | 1020 | See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) 1021 | and the [streaming response](#streaming-response) for more details, 1022 | examples and possible use-cases. 1023 | 1024 | This method will automatically add a matching `Content-Length` request 1025 | header if the size of the outgoing request body is known and non-empty. 1026 | For an empty request body, if will only include a `Content-Length: 0` 1027 | request header if the request method usually expects a request body (only 1028 | applies to `POST`, `PUT` and `PATCH`). 1029 | 1030 | If you're using a streaming request body (`ReadableStreamInterface`), it 1031 | will default to using `Transfer-Encoding: chunked` or you have to 1032 | explicitly pass in a matching `Content-Length` request header like so: 1033 | 1034 | ```php 1035 | $body = new React\Stream\ThroughStream(); 1036 | $loop->addTimer(1.0, function () use ($body) { 1037 | $body->end("hello world"); 1038 | }); 1039 | 1040 | $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); 1041 | ``` 1042 | 1043 | > Note that this method is available as of v2.9.0 and always resolves the 1044 | response without buffering the response body. 1045 | It does not respect the deprecated [`streaming` option](#withoptions). 1046 | If you want to buffer the response body, use can use the 1047 | [`request()`](#request) method instead. 1048 | 1049 | #### ~~submit()~~ 1050 | 1051 | > Deprecated since v2.9.0, see [`post()`](#post) instead. 1052 | 1053 | The deprecated `submit(string|UriInterface $url, array $fields, array $headers = array(), string $method = 'POST'): PromiseInterface` method can be used to 1054 | submit an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). 1055 | 1056 | ```php 1057 | // deprecated: see post() instead 1058 | $browser->submit($url, array('user' => 'test', 'password' => 'secret')); 1059 | ``` 1060 | 1061 | > For BC reasons, this method accepts the `$url` as either a `string` 1062 | value or as an `UriInterface`. It's recommended to explicitly cast any 1063 | objects implementing `UriInterface` to `string`. 1064 | 1065 | #### ~~send()~~ 1066 | 1067 | > Deprecated since v2.9.0, see [`request()`](#request) instead. 1068 | 1069 | The deprecated `send(RequestInterface $request): PromiseInterface` method can be used to 1070 | send an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). 1071 | 1072 | The preferred way to send an HTTP request is by using the above 1073 | [request methods](#request-methods), for example the [`get()`](#get) 1074 | method to send an HTTP `GET` request. 1075 | 1076 | As an alternative, if you want to use a custom HTTP request method, you 1077 | can use this method: 1078 | 1079 | ```php 1080 | $request = new Request('OPTIONS', $url); 1081 | 1082 | // deprecated: see request() instead 1083 | $browser->send($request)->then(…); 1084 | ``` 1085 | 1086 | This method will automatically add a matching `Content-Length` request 1087 | header if the size of the outgoing request body is known and non-empty. 1088 | For an empty request body, if will only include a `Content-Length: 0` 1089 | request header if the request method usually expects a request body (only 1090 | applies to `POST`, `PUT` and `PATCH`). 1091 | 1092 | #### withTimeout() 1093 | 1094 | The `withTimeout(bool|number $timeout): Browser` method can be used to 1095 | change the maximum timeout used for waiting for pending requests. 1096 | 1097 | You can pass in the number of seconds to use as a new timeout value: 1098 | 1099 | ```php 1100 | $browser = $browser->withTimeout(10.0); 1101 | ``` 1102 | 1103 | You can pass in a bool `false` to disable any timeouts. In this case, 1104 | requests can stay pending forever: 1105 | 1106 | ```php 1107 | $browser = $browser->withTimeout(false); 1108 | ``` 1109 | 1110 | You can pass in a bool `true` to re-enable default timeout handling. This 1111 | will respects PHP's `default_socket_timeout` setting (default 60s): 1112 | 1113 | ```php 1114 | $browser = $browser->withTimeout(true); 1115 | ``` 1116 | 1117 | See also [timeouts](#timeouts) for more details about timeout handling. 1118 | 1119 | Notice that the [`Browser`](#browser) is an immutable object, i.e. this 1120 | method actually returns a *new* [`Browser`](#browser) instance with the 1121 | given timeout value applied. 1122 | 1123 | #### withFollowRedirects() 1124 | 1125 | The `withTimeout(bool|int $$followRedirects): Browser` method can be used to 1126 | change how HTTP redirects will be followed. 1127 | 1128 | You can pass in the maximum number of redirects to follow: 1129 | 1130 | ```php 1131 | $new = $browser->withFollowRedirects(5); 1132 | ``` 1133 | 1134 | The request will automatically be rejected when the number of redirects 1135 | is exceeded. You can pass in a `0` to reject the request for any 1136 | redirects encountered: 1137 | 1138 | ```php 1139 | $browser = $browser->withFollowRedirects(0); 1140 | 1141 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 1142 | // only non-redirected responses will now end up here 1143 | var_dump($response->getHeaders()); 1144 | }); 1145 | ``` 1146 | 1147 | You can pass in a bool `false` to disable following any redirects. In 1148 | this case, requests will resolve with the redirection response instead 1149 | of following the `Location` response header: 1150 | 1151 | ```php 1152 | $browser = $browser->withFollowRedirects(false); 1153 | 1154 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 1155 | // any redirects will now end up here 1156 | var_dump($response->getHeaderLine('Location')); 1157 | }); 1158 | ``` 1159 | 1160 | You can pass in a bool `true` to re-enable default redirect handling. 1161 | This defaults to following a maximum of 10 redirects: 1162 | 1163 | ```php 1164 | $browser = $browser->withFollowRedirects(true); 1165 | ``` 1166 | 1167 | See also [redirects](#redirects) for more details about redirect handling. 1168 | 1169 | Notice that the [`Browser`](#browser) is an immutable object, i.e. this 1170 | method actually returns a *new* [`Browser`](#browser) instance with the 1171 | given redirect setting applied. 1172 | 1173 | #### withRejectErrorResponse() 1174 | 1175 | The `withRejectErrorResponse(bool $obeySuccessCode): Browser` method can be used to 1176 | change whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. 1177 | 1178 | You can pass in a bool `false` to disable rejecting incoming responses 1179 | that use a 4xx or 5xx response status code. In this case, requests will 1180 | resolve with the response message indicating an error condition: 1181 | 1182 | ```php 1183 | $browser = $browser->withRejectErrorResponse(false); 1184 | 1185 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 1186 | // any HTTP response will now end up here 1187 | var_dump($response->getStatusCode(), $response->getReasonPhrase()); 1188 | }); 1189 | ``` 1190 | 1191 | You can pass in a bool `true` to re-enable default status code handling. 1192 | This defaults to rejecting any response status codes in the 4xx or 5xx 1193 | range with a [`ResponseException`](#responseexception): 1194 | 1195 | ```php 1196 | $browser = $browser->withRejectErrorResponse(true); 1197 | 1198 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 1199 | // any successful HTTP response will now end up here 1200 | var_dump($response->getStatusCode(), $response->getReasonPhrase()); 1201 | }, function (Exception $e) { 1202 | if ($e instanceof Clue\React\Buzz\Message\ResponseException) { 1203 | // any HTTP response error message will now end up here 1204 | $response = $e->getResponse(); 1205 | var_dump($response->getStatusCode(), $response->getReasonPhrase()); 1206 | } else { 1207 | var_dump($e->getMessage()); 1208 | } 1209 | }); 1210 | ``` 1211 | 1212 | Notice that the [`Browser`](#browser) is an immutable object, i.e. this 1213 | method actually returns a *new* [`Browser`](#browser) instance with the 1214 | given setting applied. 1215 | 1216 | #### withBase() 1217 | 1218 | The `withBase(string|null|UriInterface $baseUrl): Browser` method can be used to 1219 | change the base URL used to resolve relative URLs to. 1220 | 1221 | If you configure a base URL, any requests to relative URLs will be 1222 | processed by first prepending this absolute base URL. Note that this 1223 | merely prepends the base URL and does *not* resolve any relative path 1224 | references (like `../` etc.). This is mostly useful for (RESTful) API 1225 | calls where all endpoints (URLs) are located under a common base URL. 1226 | 1227 | ```php 1228 | $browser = $browser->withBase('http://api.example.com/v3'); 1229 | 1230 | // will request http://api.example.com/v3/example 1231 | $browser->get('/example')->then(…); 1232 | ``` 1233 | 1234 | You can pass in a `null` base URL to return a new instance that does not 1235 | use a base URL: 1236 | 1237 | ```php 1238 | $browser = $browser->withBase(null); 1239 | ``` 1240 | 1241 | Accordingly, any requests using relative URLs to a browser that does not 1242 | use a base URL can not be completed and will be rejected without sending 1243 | a request. 1244 | 1245 | This method will throw an `InvalidArgumentException` if the given 1246 | `$baseUrl` argument is not a valid URL. 1247 | 1248 | Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method 1249 | actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. 1250 | 1251 | > For BC reasons, this method accepts the `$baseUrl` as either a `string` 1252 | value or as an `UriInterface`. It's recommended to explicitly cast any 1253 | objects implementing `UriInterface` to `string`. 1254 | 1255 | > Changelog: As of v2.9.0 this method accepts a `null` value to reset the 1256 | base URL. Earlier versions had to use the deprecated `withoutBase()` 1257 | method to reset the base URL. 1258 | 1259 | #### withProtocolVersion() 1260 | 1261 | The `withProtocolVersion(string $protocolVersion): Browser` method can be used to 1262 | change the HTTP protocol version that will be used for all subsequent requests. 1263 | 1264 | All the above [request methods](#request-methods) default to sending 1265 | requests as HTTP/1.1. This is the preferred HTTP protocol version which 1266 | also provides decent backwards-compatibility with legacy HTTP/1.0 1267 | servers. As such, there should rarely be a need to explicitly change this 1268 | protocol version. 1269 | 1270 | If you want to explicitly use the legacy HTTP/1.0 protocol version, you 1271 | can use this method: 1272 | 1273 | ```php 1274 | $newBrowser = $browser->withProtocolVersion('1.0'); 1275 | 1276 | $newBrowser->get($url)->then(…); 1277 | ``` 1278 | 1279 | Notice that the [`Browser`](#browser) is an immutable object, i.e. this 1280 | method actually returns a *new* [`Browser`](#browser) instance with the 1281 | new protocol version applied. 1282 | 1283 | #### withResponseBuffer() 1284 | 1285 | The `withRespomseBuffer(int $maximumSize): Browser` method can be used to 1286 | change the maximum size for buffering a response body. 1287 | 1288 | The preferred way to send an HTTP request is by using the above 1289 | [request methods](#request-methods), for example the [`get()`](#get) 1290 | method to send an HTTP `GET` request. Each of these methods will buffer 1291 | the whole response body in memory by default. This is easy to get started 1292 | and works reasonably well for smaller responses. 1293 | 1294 | By default, the response body buffer will be limited to 16 MiB. If the 1295 | response body exceeds this maximum size, the request will be rejected. 1296 | 1297 | You can pass in the maximum number of bytes to buffer: 1298 | 1299 | ```php 1300 | $browser = $browser->withResponseBuffer(1024 * 1024); 1301 | 1302 | $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 1303 | // response body will not exceed 1 MiB 1304 | var_dump($response->getHeaders(), (string) $response->getBody()); 1305 | }); 1306 | ``` 1307 | 1308 | Note that the response body buffer has to be kept in memory for each 1309 | pending request until its transfer is completed and it will only be freed 1310 | after a pending request is fulfilled. As such, increasing this maximum 1311 | buffer size to allow larger response bodies is usually not recommended. 1312 | Instead, you can use the [`requestStreaming()` method](#requeststreaming) 1313 | to receive responses with arbitrary sizes without buffering. Accordingly, 1314 | this maximum buffer size setting has no effect on streaming responses. 1315 | 1316 | Notice that the [`Browser`](#browser) is an immutable object, i.e. this 1317 | method actually returns a *new* [`Browser`](#browser) instance with the 1318 | given setting applied. 1319 | 1320 | #### ~~withOptions()~~ 1321 | 1322 | > Deprecated since v2.9.0, see [`withTimeout()`](#withtimeout), [`withFollowRedirects()`](#withfollowredirects) 1323 | and [`withRejectErrorResponse()`](#withrejecterrorresponse) instead. 1324 | 1325 | The deprecated `withOptions(array $options): Browser` method can be used to 1326 | change the options to use: 1327 | 1328 | The [`Browser`](#browser) class exposes several options for the handling of 1329 | HTTP transactions. These options resemble some of PHP's 1330 | [HTTP context options](https://www.php.net/manual/en/context.http.php) and 1331 | can be controlled via the following API (and their defaults): 1332 | 1333 | ```php 1334 | // deprecated 1335 | $newBrowser = $browser->withOptions(array( 1336 | 'timeout' => null, // see withTimeout() instead 1337 | 'followRedirects' => true, // see withFollowRedirects() instead 1338 | 'maxRedirects' => 10, // see withFollowRedirects() instead 1339 | 'obeySuccessCode' => true, // see withRejectErrorResponse() instead 1340 | 'streaming' => false, // deprecated, see requestStreaming() instead 1341 | )); 1342 | ``` 1343 | 1344 | See also [timeouts](#timeouts), [redirects](#redirects) and 1345 | [streaming](#streaming-response) for more details. 1346 | 1347 | Notice that the [`Browser`](#browser) is an immutable object, i.e. this 1348 | method actually returns a *new* [`Browser`](#browser) instance with the 1349 | options applied. 1350 | 1351 | #### ~~withoutBase()~~ 1352 | 1353 | > Deprecated since v2.9.0, see [`withBase()`](#withbase) instead. 1354 | 1355 | The deprecated `withoutBase(): Browser` method can be used to 1356 | remove the base URL. 1357 | 1358 | ```php 1359 | // deprecated: see withBase() instead 1360 | $newBrowser = $browser->withoutBase(); 1361 | ``` 1362 | 1363 | Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method 1364 | actually returns a *new* [`Browser`](#browser) instance without any base URL applied. 1365 | 1366 | See also [`withBase()`](#withbase). 1367 | 1368 | ### ResponseInterface 1369 | 1370 | The `Psr\Http\Message\ResponseInterface` represents the incoming response received from the [`Browser`](#browser). 1371 | 1372 | This is a standard interface defined in 1373 | [PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its 1374 | [`ResponseInterface` definition](https://www.php-fig.org/psr/psr-7/#3-3-psr-http-message-responseinterface) 1375 | which in turn extends the 1376 | [`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). 1377 | 1378 | ### RequestInterface 1379 | 1380 | The `Psr\Http\Message\RequestInterface` represents the outgoing request to be sent via the [`Browser`](#browser). 1381 | 1382 | This is a standard interface defined in 1383 | [PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its 1384 | [`RequestInterface` definition](https://www.php-fig.org/psr/psr-7/#3-2-psr-http-message-requestinterface) 1385 | which in turn extends the 1386 | [`MessageInterface` definition](https://www.php-fig.org/psr/psr-7/#3-1-psr-http-message-messageinterface). 1387 | 1388 | ### UriInterface 1389 | 1390 | The `Psr\Http\Message\UriInterface` represents an absolute or relative URI (aka URL). 1391 | 1392 | This is a standard interface defined in 1393 | [PSR-7: HTTP message interfaces](https://www.php-fig.org/psr/psr-7/), see its 1394 | [`UriInterface` definition](https://www.php-fig.org/psr/psr-7/#3-5-psr-http-message-uriinterface). 1395 | 1396 | > For BC reasons, the request methods accept the URL as either a `string` 1397 | value or as an `UriInterface`. It's recommended to explicitly cast any 1398 | objects implementing `UriInterface` to `string`. 1399 | 1400 | ### ResponseException 1401 | 1402 | The `ResponseException` is an `Exception` sub-class that will be used to reject 1403 | a request promise if the remote server returns a non-success status code 1404 | (anything but 2xx or 3xx). 1405 | You can control this behavior via the [`withRejectErrorResponse()` method](#withrejecterrorresponse). 1406 | 1407 | The `getCode(): int` method can be used to 1408 | return the HTTP response status code. 1409 | 1410 | The `getResponse(): ResponseInterface` method can be used to 1411 | access its underlying [`ResponseInterface`](#responseinterface) object. 1412 | 1413 | ## Install 1414 | 1415 | The recommended way to install this library is [through Composer](https://getcomposer.org). 1416 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) 1417 | 1418 | This project follows [SemVer](https://semver.org/). 1419 | This will install the latest supported version: 1420 | 1421 | ```bash 1422 | $ composer require clue/buzz-react:^2.9 1423 | ``` 1424 | 1425 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. 1426 | 1427 | This project aims to run on any platform and thus does not require any PHP 1428 | extensions and supports running on legacy PHP 5.3 through current PHP 7+ and 1429 | HHVM. 1430 | It's *highly recommended to use PHP 7+* for this project. 1431 | 1432 | ## Tests 1433 | 1434 | To run the test suite, you first need to clone this repo and then install all 1435 | dependencies [through Composer](https://getcomposer.org): 1436 | 1437 | ```bash 1438 | $ composer install 1439 | ``` 1440 | 1441 | To run the test suite, go to the project root and run: 1442 | 1443 | ```bash 1444 | $ php vendor/bin/phpunit 1445 | ``` 1446 | 1447 | The test suite also contains a number of functional integration tests that send 1448 | test HTTP requests against the online service http://httpbin.org and thus rely 1449 | on a stable internet connection. 1450 | If you do not want to run these, they can simply be skipped like this: 1451 | 1452 | ```bash 1453 | $ php vendor/bin/phpunit --exclude-group online 1454 | ``` 1455 | 1456 | ## License 1457 | 1458 | This project is released under the permissive [MIT license](LICENSE). 1459 | 1460 | > Did you know that I offer custom development services and issuing invoices for 1461 | sponsorships of releases and for contributions? Contact me (@clue) for details. 1462 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/buzz-react", 3 | "description": "Simple, async PSR-7 HTTP client for concurrently processing any number of HTTP requests, built on top of ReactPHP", 4 | "keywords": ["HTTP client", "PSR-7", "HTTP", "ReactPHP", "async"], 5 | "homepage": "https://github.com/clue/reactphp-buzz", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@clue.engineering" 11 | } 12 | ], 13 | "autoload": { 14 | "psr-4": { "Clue\\React\\Buzz\\": "src/" } 15 | }, 16 | "autoload-dev": { 17 | "psr-4": { "Clue\\Tests\\React\\Buzz\\": "tests/" } 18 | }, 19 | "require": { 20 | "php": ">=5.3", 21 | "psr/http-message": "^1.0", 22 | "react/event-loop": "^1.0 || ^0.5", 23 | "react/http-client": "^0.5.10", 24 | "react/promise": "^2.2.1 || ^1.2.1", 25 | "react/promise-stream": "^1.0 || ^0.1.2", 26 | "react/socket": "^1.1", 27 | "react/stream": "^1.0 || ^0.7", 28 | "ringcentral/psr7": "^1.2" 29 | }, 30 | "require-dev": { 31 | "clue/block-react": "^1.0", 32 | "clue/http-proxy-react": "^1.3", 33 | "clue/reactphp-ssh-proxy": "^1.0", 34 | "clue/socks-react": "^1.0", 35 | "phpunit/phpunit": "^9.0 || ^5.7 || ^4.8.35", 36 | "react/http": "^0.8" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Browser.php: -------------------------------------------------------------------------------- 1 | '127.0.0.1', 41 | * 'tcp' => array( 42 | * 'bindto' => '192.168.10.1:0' 43 | * ), 44 | * 'tls' => array( 45 | * 'verify_peer' => false, 46 | * 'verify_peer_name' => false 47 | * ) 48 | * )); 49 | * 50 | * $browser = new Clue\React\Buzz\Browser($loop, $connector); 51 | * ``` 52 | * 53 | * @param LoopInterface $loop 54 | * @param ConnectorInterface|null $connector [optional] Connector to use. 55 | * Should be `null` in order to use default Connector. 56 | */ 57 | public function __construct(LoopInterface $loop, ConnectorInterface $connector = null) 58 | { 59 | $this->messageFactory = new MessageFactory(); 60 | $this->transaction = new Transaction( 61 | Sender::createFromLoop($loop, $connector, $this->messageFactory), 62 | $this->messageFactory, 63 | $loop 64 | ); 65 | } 66 | 67 | /** 68 | * Sends an HTTP GET request 69 | * 70 | * ```php 71 | * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 72 | * var_dump((string)$response->getBody()); 73 | * }); 74 | * ``` 75 | * 76 | * See also [example 01](../examples/01-google.php). 77 | * 78 | * > For BC reasons, this method accepts the `$url` as either a `string` 79 | * value or as an `UriInterface`. It's recommended to explicitly cast any 80 | * objects implementing `UriInterface` to `string`. 81 | * 82 | * @param string|UriInterface $url URL for the request. 83 | * @param array $headers 84 | * @return PromiseInterface 85 | */ 86 | public function get($url, array $headers = array()) 87 | { 88 | return $this->requestMayBeStreaming('GET', $url, $headers); 89 | } 90 | 91 | /** 92 | * Sends an HTTP POST request 93 | * 94 | * ```php 95 | * $browser->post( 96 | * $url, 97 | * [ 98 | * 'Content-Type' => 'application/json' 99 | * ], 100 | * json_encode($data) 101 | * )->then(function (Psr\Http\Message\ResponseInterface $response) { 102 | * var_dump(json_decode((string)$response->getBody())); 103 | * }); 104 | * ``` 105 | * 106 | * See also [example 04](../examples/04-post-json.php). 107 | * 108 | * This method is also commonly used to submit HTML form data: 109 | * 110 | * ```php 111 | * $data = [ 112 | * 'user' => 'Alice', 113 | * 'password' => 'secret' 114 | * ]; 115 | * 116 | * $browser->post( 117 | * $url, 118 | * [ 119 | * 'Content-Type' => 'application/x-www-form-urlencoded' 120 | * ], 121 | * http_build_query($data) 122 | * ); 123 | * ``` 124 | * 125 | * This method will automatically add a matching `Content-Length` request 126 | * header if the outgoing request body is a `string`. If you're using a 127 | * streaming request body (`ReadableStreamInterface`), it will default to 128 | * using `Transfer-Encoding: chunked` or you have to explicitly pass in a 129 | * matching `Content-Length` request header like so: 130 | * 131 | * ```php 132 | * $body = new React\Stream\ThroughStream(); 133 | * $loop->addTimer(1.0, function () use ($body) { 134 | * $body->end("hello world"); 135 | * }); 136 | * 137 | * $browser->post($url, array('Content-Length' => '11'), $body); 138 | * ``` 139 | * 140 | * > For BC reasons, this method accepts the `$url` as either a `string` 141 | * value or as an `UriInterface`. It's recommended to explicitly cast any 142 | * objects implementing `UriInterface` to `string`. 143 | * 144 | * @param string|UriInterface $url URL for the request. 145 | * @param array $headers 146 | * @param string|ReadableStreamInterface $contents 147 | * @return PromiseInterface 148 | */ 149 | public function post($url, array $headers = array(), $contents = '') 150 | { 151 | return $this->requestMayBeStreaming('POST', $url, $headers, $contents); 152 | } 153 | 154 | /** 155 | * Sends an HTTP HEAD request 156 | * 157 | * ```php 158 | * $browser->head($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 159 | * var_dump($response->getHeaders()); 160 | * }); 161 | * ``` 162 | * 163 | * > For BC reasons, this method accepts the `$url` as either a `string` 164 | * value or as an `UriInterface`. It's recommended to explicitly cast any 165 | * objects implementing `UriInterface` to `string`. 166 | * 167 | * @param string|UriInterface $url URL for the request. 168 | * @param array $headers 169 | * @return PromiseInterface 170 | */ 171 | public function head($url, array $headers = array()) 172 | { 173 | return $this->requestMayBeStreaming('HEAD', $url, $headers); 174 | } 175 | 176 | /** 177 | * Sends an HTTP PATCH request 178 | * 179 | * ```php 180 | * $browser->patch( 181 | * $url, 182 | * [ 183 | * 'Content-Type' => 'application/json' 184 | * ], 185 | * json_encode($data) 186 | * )->then(function (Psr\Http\Message\ResponseInterface $response) { 187 | * var_dump(json_decode((string)$response->getBody())); 188 | * }); 189 | * ``` 190 | * 191 | * This method will automatically add a matching `Content-Length` request 192 | * header if the outgoing request body is a `string`. If you're using a 193 | * streaming request body (`ReadableStreamInterface`), it will default to 194 | * using `Transfer-Encoding: chunked` or you have to explicitly pass in a 195 | * matching `Content-Length` request header like so: 196 | * 197 | * ```php 198 | * $body = new React\Stream\ThroughStream(); 199 | * $loop->addTimer(1.0, function () use ($body) { 200 | * $body->end("hello world"); 201 | * }); 202 | * 203 | * $browser->patch($url, array('Content-Length' => '11'), $body); 204 | * ``` 205 | * 206 | * > For BC reasons, this method accepts the `$url` as either a `string` 207 | * value or as an `UriInterface`. It's recommended to explicitly cast any 208 | * objects implementing `UriInterface` to `string`. 209 | * 210 | * @param string|UriInterface $url URL for the request. 211 | * @param array $headers 212 | * @param string|ReadableStreamInterface $contents 213 | * @return PromiseInterface 214 | */ 215 | public function patch($url, array $headers = array(), $contents = '') 216 | { 217 | return $this->requestMayBeStreaming('PATCH', $url , $headers, $contents); 218 | } 219 | 220 | /** 221 | * Sends an HTTP PUT request 222 | * 223 | * ```php 224 | * $browser->put( 225 | * $url, 226 | * [ 227 | * 'Content-Type' => 'text/xml' 228 | * ], 229 | * $xml->asXML() 230 | * )->then(function (Psr\Http\Message\ResponseInterface $response) { 231 | * var_dump((string)$response->getBody()); 232 | * }); 233 | * ``` 234 | * 235 | * See also [example 05](../examples/05-put-xml.php). 236 | * 237 | * This method will automatically add a matching `Content-Length` request 238 | * header if the outgoing request body is a `string`. If you're using a 239 | * streaming request body (`ReadableStreamInterface`), it will default to 240 | * using `Transfer-Encoding: chunked` or you have to explicitly pass in a 241 | * matching `Content-Length` request header like so: 242 | * 243 | * ```php 244 | * $body = new React\Stream\ThroughStream(); 245 | * $loop->addTimer(1.0, function () use ($body) { 246 | * $body->end("hello world"); 247 | * }); 248 | * 249 | * $browser->put($url, array('Content-Length' => '11'), $body); 250 | * ``` 251 | * 252 | * > For BC reasons, this method accepts the `$url` as either a `string` 253 | * value or as an `UriInterface`. It's recommended to explicitly cast any 254 | * objects implementing `UriInterface` to `string`. 255 | * 256 | * @param string|UriInterface $url URL for the request. 257 | * @param array $headers 258 | * @param string|ReadableStreamInterface $contents 259 | * @return PromiseInterface 260 | */ 261 | public function put($url, array $headers = array(), $contents = '') 262 | { 263 | return $this->requestMayBeStreaming('PUT', $url, $headers, $contents); 264 | } 265 | 266 | /** 267 | * Sends an HTTP DELETE request 268 | * 269 | * ```php 270 | * $browser->delete($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 271 | * var_dump((string)$response->getBody()); 272 | * }); 273 | * ``` 274 | * 275 | * > For BC reasons, this method accepts the `$url` as either a `string` 276 | * value or as an `UriInterface`. It's recommended to explicitly cast any 277 | * objects implementing `UriInterface` to `string`. 278 | * 279 | * @param string|UriInterface $url URL for the request. 280 | * @param array $headers 281 | * @param string|ReadableStreamInterface $contents 282 | * @return PromiseInterface 283 | */ 284 | public function delete($url, array $headers = array(), $contents = '') 285 | { 286 | return $this->requestMayBeStreaming('DELETE', $url, $headers, $contents); 287 | } 288 | 289 | /** 290 | * Sends an arbitrary HTTP request. 291 | * 292 | * The preferred way to send an HTTP request is by using the above 293 | * [request methods](#request-methods), for example the [`get()`](#get) 294 | * method to send an HTTP `GET` request. 295 | * 296 | * As an alternative, if you want to use a custom HTTP request method, you 297 | * can use this method: 298 | * 299 | * ```php 300 | * $browser->request('OPTIONS', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { 301 | * var_dump((string)$response->getBody()); 302 | * }); 303 | * ``` 304 | * 305 | * This method will automatically add a matching `Content-Length` request 306 | * header if the size of the outgoing request body is known and non-empty. 307 | * For an empty request body, if will only include a `Content-Length: 0` 308 | * request header if the request method usually expects a request body (only 309 | * applies to `POST`, `PUT` and `PATCH`). 310 | * 311 | * If you're using a streaming request body (`ReadableStreamInterface`), it 312 | * will default to using `Transfer-Encoding: chunked` or you have to 313 | * explicitly pass in a matching `Content-Length` request header like so: 314 | * 315 | * ```php 316 | * $body = new React\Stream\ThroughStream(); 317 | * $loop->addTimer(1.0, function () use ($body) { 318 | * $body->end("hello world"); 319 | * }); 320 | * 321 | * $browser->request('POST', $url, array('Content-Length' => '11'), $body); 322 | * ``` 323 | * 324 | * > Note that this method is available as of v2.9.0 and always buffers the 325 | * response body before resolving. 326 | * It does not respect the deprecated [`streaming` option](#withoptions). 327 | * If you want to stream the response body, you can use the 328 | * [`requestStreaming()`](#requeststreaming) method instead. 329 | * 330 | * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. 331 | * @param string $url URL for the request 332 | * @param array $headers Additional request headers 333 | * @param string|ReadableStreamInterface $body HTTP request body contents 334 | * @return PromiseInterface 335 | * @since 2.9.0 336 | */ 337 | public function request($method, $url, array $headers = array(), $body = '') 338 | { 339 | return $this->withOptions(array('streaming' => false))->requestMayBeStreaming($method, $url, $headers, $body); 340 | } 341 | 342 | /** 343 | * Sends an arbitrary HTTP request and receives a streaming response without buffering the response body. 344 | * 345 | * The preferred way to send an HTTP request is by using the above 346 | * [request methods](#request-methods), for example the [`get()`](#get) 347 | * method to send an HTTP `GET` request. Each of these methods will buffer 348 | * the whole response body in memory by default. This is easy to get started 349 | * and works reasonably well for smaller responses. 350 | * 351 | * In some situations, it's a better idea to use a streaming approach, where 352 | * only small chunks have to be kept in memory. You can use this method to 353 | * send an arbitrary HTTP request and receive a streaming response. It uses 354 | * the same HTTP message API, but does not buffer the response body in 355 | * memory. It only processes the response body in small chunks as data is 356 | * received and forwards this data through [ReactPHP's Stream API](https://github.com/reactphp/stream). 357 | * This works for (any number of) responses of arbitrary sizes. 358 | * 359 | * ```php 360 | * $browser->requestStreaming('GET', $url)->then(function (Psr\Http\Message\ResponseInterface $response) { 361 | * $body = $response->getBody(); 362 | * assert($body instanceof Psr\Http\Message\StreamInterface); 363 | * assert($body instanceof React\Stream\ReadableStreamInterface); 364 | * 365 | * $body->on('data', function ($chunk) { 366 | * echo $chunk; 367 | * }); 368 | * 369 | * $body->on('error', function (Exception $error) { 370 | * echo 'Error: ' . $error->getMessage() . PHP_EOL; 371 | * }); 372 | * 373 | * $body->on('close', function () { 374 | * echo '[DONE]' . PHP_EOL; 375 | * }); 376 | * }); 377 | * ``` 378 | * 379 | * See also [`ReadableStreamInterface`](https://github.com/reactphp/stream#readablestreaminterface) 380 | * and the [streaming response](#streaming-response) for more details, 381 | * examples and possible use-cases. 382 | * 383 | * This method will automatically add a matching `Content-Length` request 384 | * header if the size of the outgoing request body is known and non-empty. 385 | * For an empty request body, if will only include a `Content-Length: 0` 386 | * request header if the request method usually expects a request body (only 387 | * applies to `POST`, `PUT` and `PATCH`). 388 | * 389 | * If you're using a streaming request body (`ReadableStreamInterface`), it 390 | * will default to using `Transfer-Encoding: chunked` or you have to 391 | * explicitly pass in a matching `Content-Length` request header like so: 392 | * 393 | * ```php 394 | * $body = new React\Stream\ThroughStream(); 395 | * $loop->addTimer(1.0, function () use ($body) { 396 | * $body->end("hello world"); 397 | * }); 398 | * 399 | * $browser->requestStreaming('POST', $url, array('Content-Length' => '11'), $body); 400 | * ``` 401 | * 402 | * > Note that this method is available as of v2.9.0 and always resolves the 403 | * response without buffering the response body. 404 | * It does not respect the deprecated [`streaming` option](#withoptions). 405 | * If you want to buffer the response body, use can use the 406 | * [`request()`](#request) method instead. 407 | * 408 | * @param string $method HTTP request method, e.g. GET/HEAD/POST etc. 409 | * @param string $url URL for the request 410 | * @param array $headers Additional request headers 411 | * @param string|ReadableStreamInterface $body HTTP request body contents 412 | * @return PromiseInterface 413 | * @since 2.9.0 414 | */ 415 | public function requestStreaming($method, $url, $headers = array(), $contents = '') 416 | { 417 | return $this->withOptions(array('streaming' => true))->requestMayBeStreaming($method, $url, $headers, $contents); 418 | } 419 | 420 | /** 421 | * [Deprecated] Submits an array of field values similar to submitting a form (`application/x-www-form-urlencoded`). 422 | * 423 | * ```php 424 | * // deprecated: see post() instead 425 | * $browser->submit($url, array('user' => 'test', 'password' => 'secret')); 426 | * ``` 427 | * 428 | * This method will automatically add a matching `Content-Length` request 429 | * header for the encoded length of the given `$fields`. 430 | * 431 | * > For BC reasons, this method accepts the `$url` as either a `string` 432 | * value or as an `UriInterface`. It's recommended to explicitly cast any 433 | * objects implementing `UriInterface` to `string`. 434 | * 435 | * @param string|UriInterface $url URL for the request. 436 | * @param array $fields 437 | * @param array $headers 438 | * @param string $method 439 | * @return PromiseInterface 440 | * @deprecated 2.9.0 See self::post() instead. 441 | * @see self::post() 442 | */ 443 | public function submit($url, array $fields, $headers = array(), $method = 'POST') 444 | { 445 | $headers['Content-Type'] = 'application/x-www-form-urlencoded'; 446 | $contents = http_build_query($fields); 447 | 448 | return $this->requestMayBeStreaming($method, $url, $headers, $contents); 449 | } 450 | 451 | /** 452 | * [Deprecated] Sends an arbitrary instance implementing the [`RequestInterface`](#requestinterface) (PSR-7). 453 | * 454 | * The preferred way to send an HTTP request is by using the above 455 | * [request methods](#request-methods), for example the [`get()`](#get) 456 | * method to send an HTTP `GET` request. 457 | * 458 | * As an alternative, if you want to use a custom HTTP request method, you 459 | * can use this method: 460 | * 461 | * ```php 462 | * $request = new Request('OPTIONS', $url); 463 | * 464 | * // deprecated: see request() instead 465 | * $browser->send($request)->then(…); 466 | * ``` 467 | * 468 | * This method will automatically add a matching `Content-Length` request 469 | * header if the size of the outgoing request body is known and non-empty. 470 | * For an empty request body, if will only include a `Content-Length: 0` 471 | * request header if the request method usually expects a request body (only 472 | * applies to `POST`, `PUT` and `PATCH`). 473 | * 474 | * @param RequestInterface $request 475 | * @return PromiseInterface 476 | * @deprecated 2.9.0 See self::request() instead. 477 | * @see self::request() 478 | */ 479 | public function send(RequestInterface $request) 480 | { 481 | if ($this->baseUrl !== null) { 482 | // ensure we're actually below the base URL 483 | $request = $request->withUri($this->messageFactory->expandBase($request->getUri(), $this->baseUrl)); 484 | } 485 | 486 | return $this->transaction->send($request); 487 | } 488 | 489 | /** 490 | * Changes the maximum timeout used for waiting for pending requests. 491 | * 492 | * You can pass in the number of seconds to use as a new timeout value: 493 | * 494 | * ```php 495 | * $browser = $browser->withTimeout(10.0); 496 | * ``` 497 | * 498 | * You can pass in a bool `false` to disable any timeouts. In this case, 499 | * requests can stay pending forever: 500 | * 501 | * ```php 502 | * $browser = $browser->withTimeout(false); 503 | * ``` 504 | * 505 | * You can pass in a bool `true` to re-enable default timeout handling. This 506 | * will respects PHP's `default_socket_timeout` setting (default 60s): 507 | * 508 | * ```php 509 | * $browser = $browser->withTimeout(true); 510 | * ``` 511 | * 512 | * See also [timeouts](#timeouts) for more details about timeout handling. 513 | * 514 | * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 515 | * method actually returns a *new* [`Browser`](#browser) instance with the 516 | * given timeout value applied. 517 | * 518 | * @param bool|number $timeout 519 | * @return self 520 | */ 521 | public function withTimeout($timeout) 522 | { 523 | if ($timeout === true) { 524 | $timeout = null; 525 | } elseif ($timeout === false) { 526 | $timeout = -1; 527 | } elseif ($timeout < 0) { 528 | $timeout = 0; 529 | } 530 | 531 | return $this->withOptions(array( 532 | 'timeout' => $timeout, 533 | )); 534 | } 535 | 536 | /** 537 | * Changes how HTTP redirects will be followed. 538 | * 539 | * You can pass in the maximum number of redirects to follow: 540 | * 541 | * ```php 542 | * $new = $browser->withFollowRedirects(5); 543 | * ``` 544 | * 545 | * The request will automatically be rejected when the number of redirects 546 | * is exceeded. You can pass in a `0` to reject the request for any 547 | * redirects encountered: 548 | * 549 | * ```php 550 | * $browser = $browser->withFollowRedirects(0); 551 | * 552 | * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 553 | * // only non-redirected responses will now end up here 554 | * var_dump($response->getHeaders()); 555 | * }); 556 | * ``` 557 | * 558 | * You can pass in a bool `false` to disable following any redirects. In 559 | * this case, requests will resolve with the redirection response instead 560 | * of following the `Location` response header: 561 | * 562 | * ```php 563 | * $browser = $browser->withFollowRedirects(false); 564 | * 565 | * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 566 | * // any redirects will now end up here 567 | * var_dump($response->getHeaderLine('Location')); 568 | * }); 569 | * ``` 570 | * 571 | * You can pass in a bool `true` to re-enable default redirect handling. 572 | * This defaults to following a maximum of 10 redirects: 573 | * 574 | * ```php 575 | * $browser = $browser->withFollowRedirects(true); 576 | * ``` 577 | * 578 | * See also [redirects](#redirects) for more details about redirect handling. 579 | * 580 | * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 581 | * method actually returns a *new* [`Browser`](#browser) instance with the 582 | * given redirect setting applied. 583 | * 584 | * @param bool|int $followRedirects 585 | * @return self 586 | */ 587 | public function withFollowRedirects($followRedirects) 588 | { 589 | return $this->withOptions(array( 590 | 'followRedirects' => $followRedirects !== false, 591 | 'maxRedirects' => \is_bool($followRedirects) ? null : $followRedirects 592 | )); 593 | } 594 | 595 | /** 596 | * Changes whether non-successful HTTP response status codes (4xx and 5xx) will be rejected. 597 | * 598 | * You can pass in a bool `false` to disable rejecting incoming responses 599 | * that use a 4xx or 5xx response status code. In this case, requests will 600 | * resolve with the response message indicating an error condition: 601 | * 602 | * ```php 603 | * $browser = $browser->withRejectErrorResponse(false); 604 | * 605 | * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 606 | * // any HTTP response will now end up here 607 | * var_dump($response->getStatusCode(), $response->getReasonPhrase()); 608 | * }); 609 | * ``` 610 | * 611 | * You can pass in a bool `true` to re-enable default status code handling. 612 | * This defaults to rejecting any response status codes in the 4xx or 5xx 613 | * range: 614 | * 615 | * ```php 616 | * $browser = $browser->withRejectErrorResponse(true); 617 | * 618 | * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 619 | * // any successful HTTP response will now end up here 620 | * var_dump($response->getStatusCode(), $response->getReasonPhrase()); 621 | * }, function (Exception $e) { 622 | * if ($e instanceof Clue\React\Buzz\Message\ResponseException) { 623 | * // any HTTP response error message will now end up here 624 | * $response = $e->getResponse(); 625 | * var_dump($response->getStatusCode(), $response->getReasonPhrase()); 626 | * } else { 627 | * var_dump($e->getMessage()); 628 | * } 629 | * }); 630 | * ``` 631 | * 632 | * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 633 | * method actually returns a *new* [`Browser`](#browser) instance with the 634 | * given setting applied. 635 | * 636 | * @param bool $obeySuccessCode 637 | * @return self 638 | */ 639 | public function withRejectErrorResponse($obeySuccessCode) 640 | { 641 | return $this->withOptions(array( 642 | 'obeySuccessCode' => $obeySuccessCode, 643 | )); 644 | } 645 | 646 | /** 647 | * Changes the base URL used to resolve relative URLs to. 648 | * 649 | * If you configure a base URL, any requests to relative URLs will be 650 | * processed by first prepending this absolute base URL. Note that this 651 | * merely prepends the base URL and does *not* resolve any relative path 652 | * references (like `../` etc.). This is mostly useful for (RESTful) API 653 | * calls where all endpoints (URLs) are located under a common base URL. 654 | * 655 | * ```php 656 | * $browser = $browser->withBase('http://api.example.com/v3'); 657 | * 658 | * // will request http://api.example.com/v3/example 659 | * $browser->get('/example')->then(…); 660 | * ``` 661 | * 662 | * You can pass in a `null` base URL to return a new instance that does not 663 | * use a base URL: 664 | * 665 | * ```php 666 | * $browser = $browser->withBase(null); 667 | * ``` 668 | * 669 | * Accordingly, any requests using relative URLs to a browser that does not 670 | * use a base URL can not be completed and will be rejected without sending 671 | * a request. 672 | * 673 | * This method will throw an `InvalidArgumentException` if the given 674 | * `$baseUrl` argument is not a valid URL. 675 | * 676 | * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withBase()` method 677 | * actually returns a *new* [`Browser`](#browser) instance with the given base URL applied. 678 | * 679 | * > For BC reasons, this method accepts the `$baseUrl` as either a `string` 680 | * value or as an `UriInterface`. It's recommended to explicitly cast any 681 | * objects implementing `UriInterface` to `string`. 682 | * 683 | * > Changelog: As of v2.9.0 this method accepts a `null` value to reset the 684 | * base URL. Earlier versions had to use the deprecated `withoutBase()` 685 | * method to reset the base URL. 686 | * 687 | * @param string|null|UriInterface $baseUrl absolute base URL 688 | * @return self 689 | * @throws InvalidArgumentException if the given $baseUrl is not a valid absolute URL 690 | * @see self::withoutBase() 691 | */ 692 | public function withBase($baseUrl) 693 | { 694 | $browser = clone $this; 695 | if ($baseUrl === null) { 696 | $browser->baseUrl = null; 697 | return $browser; 698 | } 699 | 700 | $browser->baseUrl = $this->messageFactory->uri($baseUrl); 701 | if (!\in_array($browser->baseUrl->getScheme(), array('http', 'https')) || $browser->baseUrl->getHost() === '') { 702 | throw new \InvalidArgumentException('Base URL must be absolute'); 703 | } 704 | 705 | return $browser; 706 | } 707 | 708 | /** 709 | * Changes the HTTP protocol version that will be used for all subsequent requests. 710 | * 711 | * All the above [request methods](#request-methods) default to sending 712 | * requests as HTTP/1.1. This is the preferred HTTP protocol version which 713 | * also provides decent backwards-compatibility with legacy HTTP/1.0 714 | * servers. As such, there should rarely be a need to explicitly change this 715 | * protocol version. 716 | * 717 | * If you want to explicitly use the legacy HTTP/1.0 protocol version, you 718 | * can use this method: 719 | * 720 | * ```php 721 | * $newBrowser = $browser->withProtocolVersion('1.0'); 722 | * 723 | * $newBrowser->get($url)->then(…); 724 | * ``` 725 | * 726 | * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 727 | * method actually returns a *new* [`Browser`](#browser) instance with the 728 | * new protocol version applied. 729 | * 730 | * @param string $protocolVersion HTTP protocol version to use, must be one of "1.1" or "1.0" 731 | * @return self 732 | * @throws InvalidArgumentException 733 | * @since 2.8.0 734 | */ 735 | public function withProtocolVersion($protocolVersion) 736 | { 737 | if (!\in_array($protocolVersion, array('1.0', '1.1'), true)) { 738 | throw new InvalidArgumentException('Invalid HTTP protocol version, must be one of "1.1" or "1.0"'); 739 | } 740 | 741 | $browser = clone $this; 742 | $browser->protocolVersion = (string) $protocolVersion; 743 | 744 | return $browser; 745 | } 746 | 747 | /** 748 | * Changes the maximum size for buffering a response body. 749 | * 750 | * The preferred way to send an HTTP request is by using the above 751 | * [request methods](#request-methods), for example the [`get()`](#get) 752 | * method to send an HTTP `GET` request. Each of these methods will buffer 753 | * the whole response body in memory by default. This is easy to get started 754 | * and works reasonably well for smaller responses. 755 | * 756 | * By default, the response body buffer will be limited to 16 MiB. If the 757 | * response body exceeds this maximum size, the request will be rejected. 758 | * 759 | * You can pass in the maximum number of bytes to buffer: 760 | * 761 | * ```php 762 | * $browser = $browser->withResponseBuffer(1024 * 1024); 763 | * 764 | * $browser->get($url)->then(function (Psr\Http\Message\ResponseInterface $response) { 765 | * // response body will not exceed 1 MiB 766 | * var_dump($response->getHeaders(), (string) $response->getBody()); 767 | * }); 768 | * ``` 769 | * 770 | * Note that the response body buffer has to be kept in memory for each 771 | * pending request until its transfer is completed and it will only be freed 772 | * after a pending request is fulfilled. As such, increasing this maximum 773 | * buffer size to allow larger response bodies is usually not recommended. 774 | * Instead, you can use the [`requestStreaming()` method](#requeststreaming) 775 | * to receive responses with arbitrary sizes without buffering. Accordingly, 776 | * this maximum buffer size setting has no effect on streaming responses. 777 | * 778 | * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 779 | * method actually returns a *new* [`Browser`](#browser) instance with the 780 | * given setting applied. 781 | * 782 | * @param int $maximumSize 783 | * @return self 784 | * @see self::requestStreaming() 785 | */ 786 | public function withResponseBuffer($maximumSize) 787 | { 788 | return $this->withOptions(array( 789 | 'maximumSize' => $maximumSize 790 | )); 791 | } 792 | 793 | /** 794 | * [Deprecated] Changes the [options](#options) to use: 795 | * 796 | * The [`Browser`](#browser) class exposes several options for the handling of 797 | * HTTP transactions. These options resemble some of PHP's 798 | * [HTTP context options](http://php.net/manual/en/context.http.php) and 799 | * can be controlled via the following API (and their defaults): 800 | * 801 | * ```php 802 | * // deprecated 803 | * $newBrowser = $browser->withOptions(array( 804 | * 'timeout' => null, // see withTimeout() instead 805 | * 'followRedirects' => true, // see withFollowRedirects() instead 806 | * 'maxRedirects' => 10, // see withFollowRedirects() instead 807 | * 'obeySuccessCode' => true, // see withRejectErrorResponse() instead 808 | * 'streaming' => false, // deprecated, see requestStreaming() instead 809 | * )); 810 | * ``` 811 | * 812 | * See also [timeouts](#timeouts), [redirects](#redirects) and 813 | * [streaming](#streaming) for more details. 814 | * 815 | * Notice that the [`Browser`](#browser) is an immutable object, i.e. this 816 | * method actually returns a *new* [`Browser`](#browser) instance with the 817 | * options applied. 818 | * 819 | * @param array $options 820 | * @return self 821 | * @deprecated 2.9.0 See self::withTimeout(), self::withFollowRedirects() and self::withRejectErrorResponse() instead. 822 | * @see self::withTimeout() 823 | * @see self::withFollowRedirects() 824 | * @see self::withRejectErrorResponse() 825 | */ 826 | public function withOptions(array $options) 827 | { 828 | $browser = clone $this; 829 | $browser->transaction = $this->transaction->withOptions($options); 830 | 831 | return $browser; 832 | } 833 | 834 | /** 835 | * [Deprecated] Removes the base URL. 836 | * 837 | * ```php 838 | * // deprecated: see withBase() instead 839 | * $newBrowser = $browser->withoutBase(); 840 | * ``` 841 | * 842 | * Notice that the [`Browser`](#browser) is an immutable object, i.e. the `withoutBase()` method 843 | * actually returns a *new* [`Browser`](#browser) instance without any base URL applied. 844 | * 845 | * See also [`withBase()`](#withbase). 846 | * 847 | * @return self 848 | * @deprecated 2.9.0 See self::withBase() instead. 849 | * @see self::withBase() 850 | */ 851 | public function withoutBase() 852 | { 853 | return $this->withBase(null); 854 | } 855 | 856 | /** 857 | * @param string $method 858 | * @param string|UriInterface $url 859 | * @param array $headers 860 | * @param string|ReadableStreamInterface $contents 861 | * @return PromiseInterface 862 | */ 863 | private function requestMayBeStreaming($method, $url, array $headers = array(), $contents = '') 864 | { 865 | return $this->send($this->messageFactory->request($method, $url, $headers, $contents, $this->protocolVersion)); 866 | } 867 | } 868 | -------------------------------------------------------------------------------- /src/Io/ChunkedEncoder.php: -------------------------------------------------------------------------------- 1 | input = $input; 26 | 27 | $this->input->on('data', array($this, 'handleData')); 28 | $this->input->on('end', array($this, 'handleEnd')); 29 | $this->input->on('error', array($this, 'handleError')); 30 | $this->input->on('close', array($this, 'close')); 31 | } 32 | 33 | public function isReadable() 34 | { 35 | return !$this->closed && $this->input->isReadable(); 36 | } 37 | 38 | public function pause() 39 | { 40 | $this->input->pause(); 41 | } 42 | 43 | public function resume() 44 | { 45 | $this->input->resume(); 46 | } 47 | 48 | public function pipe(WritableStreamInterface $dest, array $options = array()) 49 | { 50 | return Util::pipe($this, $dest, $options); 51 | } 52 | 53 | public function close() 54 | { 55 | if ($this->closed) { 56 | return; 57 | } 58 | 59 | $this->closed = true; 60 | $this->input->close(); 61 | 62 | $this->emit('close'); 63 | $this->removeAllListeners(); 64 | } 65 | 66 | /** @internal */ 67 | public function handleData($data) 68 | { 69 | if ($data !== '') { 70 | $this->emit('data', array( 71 | dechex(strlen($data)) . "\r\n" . $data . "\r\n" 72 | )); 73 | } 74 | } 75 | 76 | /** @internal */ 77 | public function handleError(\Exception $e) 78 | { 79 | $this->emit('error', array($e)); 80 | $this->close(); 81 | } 82 | 83 | /** @internal */ 84 | public function handleEnd() 85 | { 86 | $this->emit('data', array("0\r\n\r\n")); 87 | 88 | if (!$this->closed) { 89 | $this->emit('end'); 90 | $this->close(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Io/Sender.php: -------------------------------------------------------------------------------- 1 | http = $http; 68 | $this->messageFactory = $messageFactory; 69 | } 70 | 71 | /** 72 | * 73 | * @internal 74 | * @param RequestInterface $request 75 | * @return PromiseInterface Promise 76 | */ 77 | public function send(RequestInterface $request) 78 | { 79 | $body = $request->getBody(); 80 | $size = $body->getSize(); 81 | 82 | if ($size !== null && $size !== 0) { 83 | // automatically assign a "Content-Length" request header if the body size is known and non-empty 84 | $request = $request->withHeader('Content-Length', (string)$size); 85 | } elseif ($size === 0 && \in_array($request->getMethod(), array('POST', 'PUT', 'PATCH'))) { 86 | // only assign a "Content-Length: 0" request header if the body is expected for certain methods 87 | $request = $request->withHeader('Content-Length', '0'); 88 | } elseif ($body instanceof ReadableStreamInterface && $body->isReadable() && !$request->hasHeader('Content-Length')) { 89 | // use "Transfer-Encoding: chunked" when this is a streaming body and body size is unknown 90 | $request = $request->withHeader('Transfer-Encoding', 'chunked'); 91 | } else { 92 | // do not use chunked encoding if size is known or if this is an empty request body 93 | $size = 0; 94 | } 95 | 96 | $headers = array(); 97 | foreach ($request->getHeaders() as $name => $values) { 98 | $headers[$name] = implode(', ', $values); 99 | } 100 | 101 | $requestStream = $this->http->request($request->getMethod(), (string)$request->getUri(), $headers, $request->getProtocolVersion()); 102 | 103 | $deferred = new Deferred(function ($_, $reject) use ($requestStream) { 104 | // close request stream if request is cancelled 105 | $reject(new \RuntimeException('Request cancelled')); 106 | $requestStream->close(); 107 | }); 108 | 109 | $requestStream->on('error', function($error) use ($deferred) { 110 | $deferred->reject($error); 111 | }); 112 | 113 | $messageFactory = $this->messageFactory; 114 | $requestStream->on('response', function (ResponseStream $responseStream) use ($deferred, $messageFactory, $request) { 115 | // apply response header values from response stream 116 | $deferred->resolve($messageFactory->response( 117 | $responseStream->getVersion(), 118 | $responseStream->getCode(), 119 | $responseStream->getReasonPhrase(), 120 | $responseStream->getHeaders(), 121 | $responseStream, 122 | $request->getMethod() 123 | )); 124 | }); 125 | 126 | if ($body instanceof ReadableStreamInterface) { 127 | if ($body->isReadable()) { 128 | // length unknown => apply chunked transfer-encoding 129 | if ($size === null) { 130 | $body = new ChunkedEncoder($body); 131 | } 132 | 133 | // pipe body into request stream 134 | // add dummy write to immediately start request even if body does not emit any data yet 135 | $body->pipe($requestStream); 136 | $requestStream->write(''); 137 | 138 | $body->on('close', $close = function () use ($deferred, $requestStream) { 139 | $deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly')); 140 | $requestStream->close(); 141 | }); 142 | $body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) { 143 | $body->removeListener('close', $close); 144 | $deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e)); 145 | $requestStream->close(); 146 | }); 147 | $body->on('end', function () use ($close, $body) { 148 | $body->removeListener('close', $close); 149 | }); 150 | } else { 151 | // stream is not readable => end request without body 152 | $requestStream->end(); 153 | } 154 | } else { 155 | // body is fully buffered => write as one chunk 156 | $requestStream->end((string)$body); 157 | } 158 | 159 | return $deferred->promise(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Io/Transaction.php: -------------------------------------------------------------------------------- 1 | sender = $sender; 43 | $this->messageFactory = $messageFactory; 44 | $this->loop = $loop; 45 | } 46 | 47 | /** 48 | * @param array $options 49 | * @return self returns new instance, without modifying existing instance 50 | */ 51 | public function withOptions(array $options) 52 | { 53 | $transaction = clone $this; 54 | foreach ($options as $name => $value) { 55 | if (property_exists($transaction, $name)) { 56 | // restore default value if null is given 57 | if ($value === null) { 58 | $default = new self($this->sender, $this->messageFactory, $this->loop); 59 | $value = $default->$name; 60 | } 61 | 62 | $transaction->$name = $value; 63 | } 64 | } 65 | 66 | return $transaction; 67 | } 68 | 69 | public function send(RequestInterface $request) 70 | { 71 | $deferred = new Deferred(function () use (&$deferred) { 72 | if (isset($deferred->pending)) { 73 | $deferred->pending->cancel(); 74 | unset($deferred->pending); 75 | } 76 | }); 77 | 78 | $deferred->numRequests = 0; 79 | 80 | // use timeout from options or default to PHP's default_socket_timeout (60) 81 | $timeout = (float)($this->timeout !== null ? $this->timeout : ini_get("default_socket_timeout")); 82 | 83 | $loop = $this->loop; 84 | $this->next($request, $deferred)->then( 85 | function (ResponseInterface $response) use ($deferred, $loop, &$timeout) { 86 | if (isset($deferred->timeout)) { 87 | $loop->cancelTimer($deferred->timeout); 88 | unset($deferred->timeout); 89 | } 90 | $timeout = -1; 91 | $deferred->resolve($response); 92 | }, 93 | function ($e) use ($deferred, $loop, &$timeout) { 94 | if (isset($deferred->timeout)) { 95 | $loop->cancelTimer($deferred->timeout); 96 | unset($deferred->timeout); 97 | } 98 | $timeout = -1; 99 | $deferred->reject($e); 100 | } 101 | ); 102 | 103 | if ($timeout < 0) { 104 | return $deferred->promise(); 105 | } 106 | 107 | $body = $request->getBody(); 108 | if ($body instanceof ReadableStreamInterface && $body->isReadable()) { 109 | $that = $this; 110 | $body->on('close', function () use ($that, $deferred, &$timeout) { 111 | if ($timeout >= 0) { 112 | $that->applyTimeout($deferred, $timeout); 113 | } 114 | }); 115 | } else { 116 | $this->applyTimeout($deferred, $timeout); 117 | } 118 | 119 | return $deferred->promise(); 120 | } 121 | 122 | /** 123 | * @internal 124 | * @param Deferred $deferred 125 | * @param number $timeout 126 | * @return void 127 | */ 128 | public function applyTimeout(Deferred $deferred, $timeout) 129 | { 130 | $deferred->timeout = $this->loop->addTimer($timeout, function () use ($timeout, $deferred) { 131 | $deferred->reject(new \RuntimeException( 132 | 'Request timed out after ' . $timeout . ' seconds' 133 | )); 134 | if (isset($deferred->pending)) { 135 | $deferred->pending->cancel(); 136 | unset($deferred->pending); 137 | } 138 | }); 139 | } 140 | 141 | private function next(RequestInterface $request, Deferred $deferred) 142 | { 143 | $this->progress('request', array($request)); 144 | 145 | $that = $this; 146 | ++$deferred->numRequests; 147 | 148 | $promise = $this->sender->send($request); 149 | 150 | if (!$this->streaming) { 151 | $promise = $promise->then(function ($response) use ($deferred, $that) { 152 | return $that->bufferResponse($response, $deferred); 153 | }); 154 | } 155 | 156 | $deferred->pending = $promise; 157 | 158 | return $promise->then( 159 | function (ResponseInterface $response) use ($request, $that, $deferred) { 160 | return $that->onResponse($response, $request, $deferred); 161 | } 162 | ); 163 | } 164 | 165 | /** 166 | * @internal 167 | * @param ResponseInterface $response 168 | * @return PromiseInterface Promise 169 | */ 170 | public function bufferResponse(ResponseInterface $response, $deferred) 171 | { 172 | $stream = $response->getBody(); 173 | 174 | $size = $stream->getSize(); 175 | if ($size !== null && $size > $this->maximumSize) { 176 | $stream->close(); 177 | return \React\Promise\reject(new \OverflowException( 178 | 'Response body size of ' . $size . ' bytes exceeds maximum of ' . $this->maximumSize . ' bytes', 179 | \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 180 | )); 181 | } 182 | 183 | // body is not streaming => already buffered 184 | if (!$stream instanceof ReadableStreamInterface) { 185 | return \React\Promise\resolve($response); 186 | } 187 | 188 | // buffer stream and resolve with buffered body 189 | $messageFactory = $this->messageFactory; 190 | $maximumSize = $this->maximumSize; 191 | $promise = \React\Promise\Stream\buffer($stream, $maximumSize)->then( 192 | function ($body) use ($response, $messageFactory) { 193 | return $response->withBody($messageFactory->body($body)); 194 | }, 195 | function ($e) use ($stream, $maximumSize) { 196 | // try to close stream if buffering fails (or is cancelled) 197 | $stream->close(); 198 | 199 | if ($e instanceof \OverflowException) { 200 | $e = new \OverflowException( 201 | 'Response body size exceeds maximum of ' . $maximumSize . ' bytes', 202 | \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 0 203 | ); 204 | } 205 | 206 | throw $e; 207 | } 208 | ); 209 | 210 | $deferred->pending = $promise; 211 | 212 | return $promise; 213 | } 214 | 215 | /** 216 | * @internal 217 | * @param ResponseInterface $response 218 | * @param RequestInterface $request 219 | * @throws ResponseException 220 | * @return ResponseInterface|PromiseInterface 221 | */ 222 | public function onResponse(ResponseInterface $response, RequestInterface $request, $deferred) 223 | { 224 | $this->progress('response', array($response, $request)); 225 | 226 | // follow 3xx (Redirection) response status codes if Location header is present and not explicitly disabled 227 | // @link https://tools.ietf.org/html/rfc7231#section-6.4 228 | if ($this->followRedirects && ($response->getStatusCode() >= 300 && $response->getStatusCode() < 400) && $response->hasHeader('Location')) { 229 | return $this->onResponseRedirect($response, $request, $deferred); 230 | } 231 | 232 | // only status codes 200-399 are considered to be valid, reject otherwise 233 | if ($this->obeySuccessCode && ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400)) { 234 | throw new ResponseException($response); 235 | } 236 | 237 | // resolve our initial promise 238 | return $response; 239 | } 240 | 241 | /** 242 | * @param ResponseInterface $response 243 | * @param RequestInterface $request 244 | * @return PromiseInterface 245 | * @throws \RuntimeException 246 | */ 247 | private function onResponseRedirect(ResponseInterface $response, RequestInterface $request, Deferred $deferred) 248 | { 249 | // resolve location relative to last request URI 250 | $location = $this->messageFactory->uriRelative($request->getUri(), $response->getHeaderLine('Location')); 251 | 252 | $request = $this->makeRedirectRequest($request, $location); 253 | $this->progress('redirect', array($request)); 254 | 255 | if ($deferred->numRequests >= $this->maxRedirects) { 256 | throw new \RuntimeException('Maximum number of redirects (' . $this->maxRedirects . ') exceeded'); 257 | } 258 | 259 | return $this->next($request, $deferred); 260 | } 261 | 262 | /** 263 | * @param RequestInterface $request 264 | * @param UriInterface $location 265 | * @return RequestInterface 266 | */ 267 | private function makeRedirectRequest(RequestInterface $request, UriInterface $location) 268 | { 269 | $originalHost = $request->getUri()->getHost(); 270 | $request = $request 271 | ->withoutHeader('Host') 272 | ->withoutHeader('Content-Type') 273 | ->withoutHeader('Content-Length'); 274 | 275 | // Remove authorization if changing hostnames (but not if just changing ports or protocols). 276 | if ($location->getHost() !== $originalHost) { 277 | $request = $request->withoutHeader('Authorization'); 278 | } 279 | 280 | // naïve approach.. 281 | $method = ($request->getMethod() === 'HEAD') ? 'HEAD' : 'GET'; 282 | 283 | return $this->messageFactory->request($method, $location, $request->getHeaders()); 284 | } 285 | 286 | private function progress($name, array $args = array()) 287 | { 288 | return; 289 | 290 | echo $name; 291 | 292 | foreach ($args as $arg) { 293 | echo ' '; 294 | if ($arg instanceof ResponseInterface) { 295 | echo 'HTTP/' . $arg->getProtocolVersion() . ' ' . $arg->getStatusCode() . ' ' . $arg->getReasonPhrase(); 296 | } elseif ($arg instanceof RequestInterface) { 297 | echo $arg->getMethod() . ' ' . $arg->getRequestTarget() . ' HTTP/' . $arg->getProtocolVersion(); 298 | } else { 299 | echo $arg; 300 | } 301 | } 302 | 303 | echo PHP_EOL; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /src/Message/MessageFactory.php: -------------------------------------------------------------------------------- 1 | body($content), $protocolVersion); 30 | } 31 | 32 | /** 33 | * Creates a new instance of ResponseInterface for the given response parameters 34 | * 35 | * @param string $protocolVersion 36 | * @param int $status 37 | * @param string $reason 38 | * @param array $headers 39 | * @param ReadableStreamInterface|string $body 40 | * @param ?string $requestMethod 41 | * @return Response 42 | * @uses self::body() 43 | */ 44 | public function response($protocolVersion, $status, $reason, $headers = array(), $body = '', $requestMethod = null) 45 | { 46 | $response = new Response($status, $headers, $body instanceof ReadableStreamInterface ? null : $body, $protocolVersion, $reason); 47 | 48 | if ($body instanceof ReadableStreamInterface) { 49 | $length = null; 50 | $code = $response->getStatusCode(); 51 | if ($requestMethod === 'HEAD' || ($code >= 100 && $code < 200) || $code == 204 || $code == 304) { 52 | $length = 0; 53 | } elseif (\strtolower($response->getHeaderLine('Transfer-Encoding')) === 'chunked') { 54 | $length = null; 55 | } elseif ($response->hasHeader('Content-Length')) { 56 | $length = (int)$response->getHeaderLine('Content-Length'); 57 | } 58 | 59 | $response = $response->withBody(new ReadableBodyStream($body, $length)); 60 | } 61 | 62 | return $response; 63 | } 64 | 65 | /** 66 | * Creates a new instance of StreamInterface for the given body contents 67 | * 68 | * @param ReadableStreamInterface|string $body 69 | * @return StreamInterface 70 | */ 71 | public function body($body) 72 | { 73 | if ($body instanceof ReadableStreamInterface) { 74 | return new ReadableBodyStream($body); 75 | } 76 | 77 | return \RingCentral\Psr7\stream_for($body); 78 | } 79 | 80 | /** 81 | * Creates a new instance of UriInterface for the given URI string 82 | * 83 | * @param string $uri 84 | * @return UriInterface 85 | */ 86 | public function uri($uri) 87 | { 88 | return new Uri($uri); 89 | } 90 | 91 | /** 92 | * Creates a new instance of UriInterface for the given URI string relative to the given base URI 93 | * 94 | * @param UriInterface $base 95 | * @param string $uri 96 | * @return UriInterface 97 | */ 98 | public function uriRelative(UriInterface $base, $uri) 99 | { 100 | return Uri::resolve($base, $uri); 101 | } 102 | 103 | /** 104 | * Resolves the given relative or absolute $uri by appending it behind $this base URI 105 | * 106 | * The given $uri parameter can be either a relative or absolute URI and 107 | * as such can not contain any URI template placeholders. 108 | * 109 | * As such, the outcome of this method represents a valid, absolute URI 110 | * which will be returned as an instance implementing `UriInterface`. 111 | * 112 | * If the given $uri is a relative URI, it will simply be appended behind $base URI. 113 | * 114 | * If the given $uri is an absolute URI, it will simply be returned as-is. 115 | * 116 | * @param UriInterface $uri 117 | * @param UriInterface $base 118 | * @return UriInterface 119 | */ 120 | public function expandBase(UriInterface $uri, UriInterface $base) 121 | { 122 | if ($uri->getScheme() !== '') { 123 | return $uri; 124 | } 125 | 126 | $uri = (string)$uri; 127 | $base = (string)$base; 128 | 129 | if ($uri !== '' && substr($base, -1) !== '/' && substr($uri, 0, 1) !== '?') { 130 | $base .= '/'; 131 | } 132 | 133 | if (isset($uri[0]) && $uri[0] === '/') { 134 | $uri = substr($uri, 1); 135 | } 136 | 137 | return $this->uri($base . $uri); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Message/ReadableBodyStream.php: -------------------------------------------------------------------------------- 1 | input = $input; 24 | $this->size = $size; 25 | 26 | $that = $this; 27 | $pos =& $this->position; 28 | $input->on('data', function ($data) use ($that, &$pos, $size) { 29 | $that->emit('data', array($data)); 30 | 31 | $pos += \strlen($data); 32 | if ($size !== null && $pos >= $size) { 33 | $that->handleEnd(); 34 | } 35 | }); 36 | $input->on('error', function ($error) use ($that) { 37 | $that->emit('error', array($error)); 38 | $that->close(); 39 | }); 40 | $input->on('end', array($that, 'handleEnd')); 41 | $input->on('close', array($that, 'close')); 42 | } 43 | 44 | public function close() 45 | { 46 | if (!$this->closed) { 47 | $this->closed = true; 48 | $this->input->close(); 49 | 50 | $this->emit('close'); 51 | $this->removeAllListeners(); 52 | } 53 | } 54 | 55 | public function isReadable() 56 | { 57 | return $this->input->isReadable(); 58 | } 59 | 60 | public function pause() 61 | { 62 | $this->input->pause(); 63 | } 64 | 65 | public function resume() 66 | { 67 | $this->input->resume(); 68 | } 69 | 70 | public function pipe(WritableStreamInterface $dest, array $options = array()) 71 | { 72 | Util::pipe($this, $dest, $options); 73 | 74 | return $dest; 75 | } 76 | 77 | public function eof() 78 | { 79 | return !$this->isReadable(); 80 | } 81 | 82 | public function __toString() 83 | { 84 | return ''; 85 | } 86 | 87 | public function detach() 88 | { 89 | throw new \BadMethodCallException(); 90 | } 91 | 92 | public function getSize() 93 | { 94 | return $this->size; 95 | } 96 | 97 | public function tell() 98 | { 99 | throw new \BadMethodCallException(); 100 | } 101 | 102 | public function isSeekable() 103 | { 104 | return false; 105 | } 106 | 107 | public function seek($offset, $whence = SEEK_SET) 108 | { 109 | throw new \BadMethodCallException(); 110 | } 111 | 112 | public function rewind() 113 | { 114 | throw new \BadMethodCallException(); 115 | } 116 | 117 | public function isWritable() 118 | { 119 | return false; 120 | } 121 | 122 | public function write($string) 123 | { 124 | throw new \BadMethodCallException(); 125 | } 126 | 127 | public function read($length) 128 | { 129 | throw new \BadMethodCallException(); 130 | } 131 | 132 | public function getContents() 133 | { 134 | throw new \BadMethodCallException(); 135 | } 136 | 137 | public function getMetadata($key = null) 138 | { 139 | return ($key === null) ? array() : null; 140 | } 141 | 142 | /** @internal */ 143 | public function handleEnd() 144 | { 145 | if ($this->position !== $this->size && $this->size !== null) { 146 | $this->emit('error', array(new \UnderflowException('Unexpected end of response body after ' . $this->position . '/' . $this->size . ' bytes'))); 147 | } else { 148 | $this->emit('end'); 149 | } 150 | 151 | $this->close(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Message/ResponseException.php: -------------------------------------------------------------------------------- 1 | getStatusCode() . ' (' . $response->getReasonPhrase() . ')'; 25 | } 26 | if ($code === null) { 27 | $code = $response->getStatusCode(); 28 | } 29 | parent::__construct($message, $code, $previous); 30 | 31 | $this->response = $response; 32 | } 33 | 34 | /** 35 | * Access its underlying [`ResponseInterface`](#responseinterface) object. 36 | * 37 | * @return ResponseInterface 38 | */ 39 | public function getResponse() 40 | { 41 | return $this->response; 42 | } 43 | } 44 | --------------------------------------------------------------------------------