friendship ended with social-app. php is my new best friend
1<?php 2 3namespace React\Http\Io; 4 5use Psr\Http\Message\RequestInterface; 6use Psr\Http\Message\ResponseInterface; 7use React\EventLoop\LoopInterface; 8use React\Http\Client\Client as HttpClient; 9use React\Promise\PromiseInterface; 10use React\Promise\Deferred; 11use React\Socket\ConnectorInterface; 12use React\Stream\ReadableStreamInterface; 13 14/** 15 * [Internal] Sends requests and receives responses 16 * 17 * The `Sender` is responsible for passing the [`RequestInterface`](#requestinterface) objects to 18 * the underlying [`HttpClient`](https://github.com/reactphp/http-client) library 19 * and keeps track of its transmission and converts its reponses back to [`ResponseInterface`](#responseinterface) objects. 20 * 21 * It also registers everything with the main [`EventLoop`](https://github.com/reactphp/event-loop#usage) 22 * and the default [`Connector`](https://github.com/reactphp/socket-client) and [DNS `Resolver`](https://github.com/reactphp/dns). 23 * 24 * The `Sender` class mostly exists in order to abstract changes on the underlying 25 * components away from this package in order to provide backwards and forwards 26 * compatibility. 27 * 28 * @internal You SHOULD NOT rely on this API, it is subject to change without prior notice! 29 * @see Browser 30 */ 31class Sender 32{ 33 /** 34 * create a new default sender attached to the given event loop 35 * 36 * This method is used internally to create the "default sender". 37 * 38 * You may also use this method if you need custom DNS or connector 39 * settings. You can use this method manually like this: 40 * 41 * ```php 42 * $connector = new \React\Socket\Connector(array(), $loop); 43 * $sender = \React\Http\Io\Sender::createFromLoop($loop, $connector); 44 * ``` 45 * 46 * @param LoopInterface $loop 47 * @param ConnectorInterface|null $connector 48 * @return self 49 */ 50 public static function createFromLoop(LoopInterface $loop, ConnectorInterface $connector) 51 { 52 return new self(new HttpClient(new ClientConnectionManager($connector, $loop))); 53 } 54 55 private $http; 56 57 /** 58 * [internal] Instantiate Sender 59 * 60 * @param HttpClient $http 61 * @internal 62 */ 63 public function __construct(HttpClient $http) 64 { 65 $this->http = $http; 66 } 67 68 /** 69 * 70 * @internal 71 * @param RequestInterface $request 72 * @return PromiseInterface Promise<ResponseInterface, Exception> 73 */ 74 public function send(RequestInterface $request) 75 { 76 // support HTTP/1.1 and HTTP/1.0 only, ensured by `Browser` already 77 assert(\in_array($request->getProtocolVersion(), array('1.0', '1.1'), true)); 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 && $size !== 0 && $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 // automatically add `Authorization: Basic …` request header if URL includes `user:pass@host` 97 if ($request->getUri()->getUserInfo() !== '' && !$request->hasHeader('Authorization')) { 98 $request = $request->withHeader('Authorization', 'Basic ' . \base64_encode($request->getUri()->getUserInfo())); 99 } 100 101 $requestStream = $this->http->request($request); 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 $requestStream->on('response', function (ResponseInterface $response) use ($deferred, $request) { 114 $deferred->resolve($response); 115 }); 116 117 if ($body instanceof ReadableStreamInterface) { 118 if ($body->isReadable()) { 119 // length unknown => apply chunked transfer-encoding 120 if ($size === null) { 121 $body = new ChunkedEncoder($body); 122 } 123 124 // pipe body into request stream 125 // add dummy write to immediately start request even if body does not emit any data yet 126 $body->pipe($requestStream); 127 $requestStream->write(''); 128 129 $body->on('close', $close = function () use ($deferred, $requestStream) { 130 $deferred->reject(new \RuntimeException('Request failed because request body closed unexpectedly')); 131 $requestStream->close(); 132 }); 133 $body->on('error', function ($e) use ($deferred, $requestStream, $close, $body) { 134 $body->removeListener('close', $close); 135 $deferred->reject(new \RuntimeException('Request failed because request body reported an error', 0, $e)); 136 $requestStream->close(); 137 }); 138 $body->on('end', function () use ($close, $body) { 139 $body->removeListener('close', $close); 140 }); 141 } else { 142 // stream is not readable => end request without body 143 $requestStream->end(); 144 } 145 } else { 146 // body is fully buffered => write as one chunk 147 $requestStream->end((string)$body); 148 } 149 150 return $deferred->promise(); 151 } 152}