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}