├── .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 [](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 |
--------------------------------------------------------------------------------