Skip to content

Commit c02fc4b

Browse files
authored
Merge pull request #342 from clue-labs/docs
Improve documentation for concurrency and streaming requests and for error handling
2 parents 0eb5988 + ff78fd0 commit c02fc4b

File tree

3 files changed

+337
-130
lines changed

3 files changed

+337
-130
lines changed

README.md

Lines changed: 209 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ Event-driven, streaming plaintext HTTP and secure HTTPS server for [ReactPHP](ht
1010
* [Usage](#usage)
1111
* [Server](#server)
1212
* [StreamingServer](#streamingserver)
13+
* [listen()](#listen)
1314
* [Request](#request)
1415
* [Request parameters](#request-parameters)
1516
* [Query parameters](#query-parameters)
1617
* [Request body](#request-body)
1718
* [Streaming request](#streaming-request)
1819
* [Request method](#request-method)
1920
* [Cookie parameters](#cookie-parameters)
21+
* [Invalid request](#invalid-request)
2022
* [Response](#response)
2123
* [Deferred response](#deferred-response)
2224
* [Streaming response](#streaming-response)
@@ -65,11 +67,10 @@ The `Server` class is responsible for handling incoming connections and then
6567
processing each incoming HTTP request.
6668

6769
It buffers and parses the complete incoming HTTP request in memory. Once the
68-
complete request has been received, it will invoke the request handler.
69-
70-
For each request, it executes the callback function passed to the
71-
constructor with the respective [request](#request) object and expects
72-
a respective [response](#response) object in return.
70+
complete request has been received, it will invoke the request handler function.
71+
This request handler function needs to be passed to the constructor and will be
72+
invoked with the respective [request](#request) object and expects a
73+
[response](#response) object in return:
7374

7475
```php
7576
$server = new Server(function (ServerRequestInterface $request) {
@@ -83,18 +84,79 @@ $server = new Server(function (ServerRequestInterface $request) {
8384
});
8485
```
8586

86-
For most users a server that buffers and parses a requests before handling it over as a
87-
PSR-7 request is what they want. The `Server` facade takes care of that, and takes the more
88-
advanced configuration out of hand. Under the hood it uses [StreamingServer](#streamingserver)
89-
with the the three stock middleware using default settings from `php.ini`.
87+
Each incoming HTTP request message is always represented by the
88+
[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface),
89+
see also following [request](#request) chapter for more details.
90+
Each outgoing HTTP response message is always represented by the
91+
[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface),
92+
see also following [response](#response) chapter for more details.
93+
94+
In order to process any connections, the server needs to be attached to an
95+
instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method
96+
as described in the following chapter. In its most simple form, you can attach
97+
this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server)
98+
in order to start a plaintext HTTP server like this:
99+
100+
```php
101+
$server = new Server($handler);
102+
103+
$socket = new React\Socket\Server('0.0.0.0:8080', $loop);
104+
$server->listen($socket);
105+
```
90106

91-
The [LimitConcurrentRequestsMiddleware](#limitconcurrentrequestsmiddleware) requires a limit,
92-
as such the `Server` facade uses the `memory_limit` and `post_max_size` ini settings to
93-
calculate a sensible limit. It assumes a maximum of a quarter of the `memory_limit` for
94-
buffering and the other three quarter for parsing and handling the requests. The limit is
95-
division of half of `memory_limit` by `memory_limit` rounded up.
107+
See also the [`listen()`](#listen) method and the [first example](examples) for more details.
96108

97-
> Note that any errors emitted by the wrapped `StreamingServer` are forwarded by `Server`.
109+
The `Server` class is built as a facade around the underlying
110+
[`StreamingServer`](#streamingserver) to provide sane defaults for 80% of the
111+
use cases and is the recommended way to use this library unless you're sure
112+
you know what you're doing.
113+
114+
Unlike the underlying [`StreamingServer`](#streamingserver), this class
115+
buffers and parses the complete incoming HTTP request in memory. Once the
116+
complete request has been received, it will invoke the request handler
117+
function. This means the [request](#request) passed to your request handler
118+
function will be fully compatible with PSR-7.
119+
120+
On the other hand, buffering complete HTTP requests in memory until they can
121+
be processed by your request handler function means that this class has to
122+
employ a number of limits to avoid consuming too much memory. In order to
123+
take the more advanced configuration out your hand, it respects setting from
124+
your [`php.ini`](https://www.php.net/manual/en/ini.core.php) to apply its
125+
default settings. This is a list of PHP settings this class respects with
126+
their respective default values:
127+
128+
```
129+
memory_limit 128M
130+
post_max_size 8M
131+
enable_post_data_reading 1
132+
max_input_nesting_level 64
133+
max_input_vars 1000
134+
135+
file_uploads 1
136+
upload_max_filesize 2M
137+
max_file_uploads 20
138+
```
139+
140+
In particular, the `post_max_size` setting limits how much memory a single HTTP
141+
request is allowed to consume while buffering its request body. On top of
142+
this, this class will try to avoid consuming more than 1/4 of your
143+
`memory_limit` for buffering multiple concurrent HTTP requests. As such, with
144+
the above default settings of `128M` max, it will try to consume no more than
145+
`32M` for buffering multiple concurrent HTTP requests. As a consequence, it
146+
will limit the concurrency to 4 HTTP requests with the above defaults.
147+
148+
It is imperative that you assign reasonable values to your PHP ini settings.
149+
It is usually recommended to either reduce the memory a single request is
150+
allowed to take (set `post_max_size 1M` or less) or to increase the total memory
151+
limit to allow for more concurrent requests (set `memory_limit 512M` or more).
152+
Failure to do so means that this class may have to disable concurrency and
153+
only handle one request at a time.
154+
155+
Internally, this class automatically assigns these limits to the
156+
[middleware](#middleware) request handlers as described below. For more
157+
advanced use cases, you may also use the advanced
158+
[`StreamingServer`](#streamingserver) and assign these middleware request
159+
handlers yourself as described in the following chapters.
98160

99161
### StreamingServer
100162

@@ -103,11 +165,11 @@ processing each incoming HTTP request.
103165

104166
Unlike the [`Server`](#server) class, it does not buffer and parse the incoming
105167
HTTP request body by default. This means that the request handler will be
106-
invoked with a streaming request body.
107-
108-
For each request, it executes the callback function passed to the
109-
constructor with the respective [request](#request) object and expects
110-
a respective [response](#response) object in return.
168+
invoked with a streaming request body. Once the request headers have been
169+
received, it will invoke the request handler function. This request handler
170+
function needs to be passed to the constructor and will be invoked with the
171+
respective [request](#request) object and expects a [response](#response)
172+
object in return:
111173

112174
```php
113175
$server = new StreamingServer(function (ServerRequestInterface $request) {
@@ -121,82 +183,88 @@ $server = new StreamingServer(function (ServerRequestInterface $request) {
121183
});
122184
```
123185

124-
In order to process any connections, the server needs to be attached to an
125-
instance of `React\Socket\ServerInterface` which emits underlying streaming
126-
connections in order to then parse incoming data as HTTP.
186+
Each incoming HTTP request message is always represented by the
187+
[PSR-7 `ServerRequestInterface`](https://www.php-fig.org/psr/psr-7/#321-psrhttpmessageserverrequestinterface),
188+
see also following [request](#request) chapter for more details.
189+
Each outgoing HTTP response message is always represented by the
190+
[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface),
191+
see also following [response](#response) chapter for more details.
127192

128-
You can attach this to a
129-
[`React\Socket\Server`](https://github.com/reactphp/socket#server)
193+
In order to process any connections, the server needs to be attached to an
194+
instance of `React\Socket\ServerInterface` through the [`listen()`](#listen) method
195+
as described in the following chapter. In its most simple form, you can attach
196+
this to a [`React\Socket\Server`](https://github.com/reactphp/socket#server)
130197
in order to start a plaintext HTTP server like this:
131198

132199
```php
133200
$server = new StreamingServer($handler);
134201

135-
$socket = new React\Socket\Server(8080, $loop);
202+
$socket = new React\Socket\Server('0.0.0.0:8080', $loop);
136203
$server->listen($socket);
137204
```
138205

139-
See also the `listen()` method and the [first example](examples) for more details.
140-
141-
Similarly, you can also attach this to a
142-
[`React\Socket\SecureServer`](https://github.com/reactphp/socket#secureserver)
143-
in order to start a secure HTTPS server like this:
206+
See also the [`listen()`](#listen) method and the [first example](examples) for more details.
207+
208+
The `StreamingServer` class is considered advanced usage and unless you know
209+
what you're doing, you're recommended to use the [`Server`](#server) class
210+
instead. The `StreamingServer` class is specifically designed to help with
211+
more advanced use cases where you want to have full control over consuming
212+
the incoming HTTP request body and concurrency settings.
213+
214+
In particular, this class does not buffer and parse the incoming HTTP request
215+
in memory. It will invoke the request handler function once the HTTP request
216+
headers have been received, i.e. before receiving the potentially much larger
217+
HTTP request body. This means the [request](#request) passed to your request
218+
handler function may not be fully compatible with PSR-7. See also
219+
[streaming request](#streaming-request) below for more details.
220+
221+
### listen()
222+
223+
The `listen(React\Socket\ServerInterface $socket): void` method can be used to
224+
start processing connections from the given socket server.
225+
The given [`React\Socket\ServerInterface`](https://github.com/reactphp/socket#serverinterface)
226+
is responsible for emitting the underlying streaming connections.
227+
This HTTP server needs to be attached to it in order to process any connections
228+
and pase incoming streaming data as incoming HTTP request messages.
229+
In its most common form, you can attach this to a
230+
[`React\Socket\Server`](https://github.com/reactphp/socket#server)
231+
in order to start a plaintext HTTP server like this:
144232

145233
```php
234+
$server = new Server($handler);
235+
// or
146236
$server = new StreamingServer($handler);
147237

148-
$socket = new React\Socket\Server(8080, $loop);
149-
$socket = new React\Socket\SecureServer($socket, $loop, array(
150-
'local_cert' => __DIR__ . '/localhost.pem'
151-
));
152-
238+
$socket = new React\Socket\Server('0.0.0.0:8080', $loop);
153239
$server->listen($socket);
154240
```
155241

156-
See also [example #11](examples) for more details.
157-
158-
When HTTP/1.1 clients want to send a bigger request body, they MAY send only
159-
the request headers with an additional `Expect: 100-continue` header and
160-
wait before sending the actual (large) message body.
161-
In this case the server will automatically send an intermediary
162-
`HTTP/1.1 100 Continue` response to the client.
163-
This ensures you will receive the request body without a delay as expected.
164-
The [Response](#response) still needs to be created as described in the
165-
examples above.
242+
This example will start listening for HTTP requests on the alternative HTTP port
243+
`8080` on all interfaces (publicly). As an alternative, it is very common to use
244+
a reverse proxy and let this HTTP server listen on the localhost (loopback)
245+
interface only by using the listen address `127.0.0.1:8080` instead. This way, you
246+
host your application(s) on the default HTTP port `80` and only route specific
247+
requests to this HTTP server.
166248

167-
See also [request](#request) and [response](#response)
168-
for more details (e.g. the request data body).
169-
170-
The `StreamingServer` supports both HTTP/1.1 and HTTP/1.0 request messages.
171-
If a client sends an invalid request message, uses an invalid HTTP protocol
172-
version or sends an invalid `Transfer-Encoding` in the request header, it will
173-
emit an `error` event, send an HTTP error response to the client and
174-
close the connection:
249+
Likewise, it's usually recommended to use a reverse proxy setup to accept
250+
secure HTTPS requests on default HTTPS port `443` (TLS termination) and only
251+
route plaintext requests to this HTTP server. As an alternative, you can also
252+
accept secure HTTPS requests with this HTTP server by attaching this to a
253+
[`React\Socket\Server`](https://github.com/reactphp/socket#server) using a
254+
secure TLS listen address, a certificate file and optional `passphrase` like this:
175255

176256
```php
177-
$server->on('error', function (Exception $e) {
178-
echo 'Error: ' . $e->getMessage() . PHP_EOL;
179-
});
180-
```
181-
182-
The server will also emit an `error` event if you return an invalid
183-
type in the callback function or have a unhandled `Exception` or `Throwable`.
184-
If your callback function throws an `Exception` or `Throwable`,
185-
the `StreamingServer` will emit a `RuntimeException` and add the thrown exception
186-
as previous:
257+
$server = new Server($handler);
258+
// or
259+
$server = new StreamingServer($handler);
187260

188-
```php
189-
$server->on('error', function (Exception $e) {
190-
echo 'Error: ' . $e->getMessage() . PHP_EOL;
191-
if ($e->getPrevious() !== null) {
192-
$previousException = $e->getPrevious();
193-
echo $previousException->getMessage() . PHP_EOL;
194-
}
195-
});
261+
$socket = new React\Socket\Server('tls://0.0.0.0:8443', $loop, array(
262+
'local_cert' => __DIR__ . '/localhost.pem'
263+
));
264+
$server->listen($socket);
196265
```
197266

198-
Note that the request object can also emit an error.
199-
Check out [request](#request) for more details.
267+
See also [example #11](examples) for more details.
200268

201269
### Request
202270

@@ -405,6 +473,14 @@ This method operates on the buffered request body, i.e. the request body size
405473
is always known, even when the request does not specify a `Content-Length` request
406474
header or when using `Transfer-Encoding: chunked` for HTTP/1.1 requests.
407475

476+
> Note: The `Server` automatically takes care of handling requests with the
477+
additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to
478+
send a bigger request body, they MAY send only the request headers with an
479+
additional `Expect: 100-continue` request header and wait before sending the actual
480+
(large) message body. In this case the server will automatically send an
481+
intermediary `HTTP/1.1 100 Continue` response to the client. This ensures you
482+
will receive the request body without a delay as expected.
483+
408484
#### Streaming request
409485

410486
If you're using the advanced [`StreamingServer`](#streamingserver), the
@@ -536,6 +612,14 @@ $server = new StreamingServer(function (ServerRequestInterface $request) {
536612
});
537613
```
538614

615+
> Note: The `StreamingServer` automatically takes care of handling requests with the
616+
additional `Expect: 100-continue` request header. When HTTP/1.1 clients want to
617+
send a bigger request body, they MAY send only the request headers with an
618+
additional `Expect: 100-continue` request header and wait before sending the actual
619+
(large) message body. In this case the server will automatically send an
620+
intermediary `HTTP/1.1 100 Continue` response to the client. This ensures you
621+
will receive the streaming request body without a delay as expected.
622+
539623
#### Request method
540624

541625
Note that the server supports *any* request method (including custom and non-
@@ -608,6 +692,26 @@ This encoding is also used internally when decoding the name and value of cookie
608692

609693
See also [example #5](examples) for more details.
610694

695+
#### Invalid request
696+
697+
The `Server` and `StreamingServer` classes support both HTTP/1.1 and HTTP/1.0 request
698+
messages. If a client sends an invalid request message, uses an invalid HTTP
699+
protocol version or sends an invalid `Transfer-Encoding` request header value,
700+
the server will automatically send a `400` (Bad Request) HTTP error response
701+
to the client and close the connection.
702+
On top of this, it will emit an `error` event that can be used for logging
703+
purposes like this:
704+
705+
```php
706+
$server->on('error', function (Exception $e) {
707+
echo 'Error: ' . $e->getMessage() . PHP_EOL;
708+
});
709+
```
710+
711+
Note that the server will also emit an `error` event if you do not return a
712+
valid response object from your request handler function. See also
713+
[invalid response](#invalid-response) for more details.
714+
611715
### Response
612716

613717
The callback function passed to the constructor of the [`Server`](#server) or
@@ -831,9 +935,37 @@ to the message if the same request would have used an (unconditional) `GET`.
831935

832936
#### Invalid response
833937

834-
An invalid return value or an unhandled `Exception` or `Throwable` in the code
835-
of the callback function, will result in an `500 Internal Server Error` message.
836-
Make sure to catch `Exceptions` or `Throwables` to create own response messages.
938+
As stated above, each outgoing HTTP response is always represented by the
939+
[PSR-7 `ResponseInterface`](https://www.php-fig.org/psr/psr-7/#33-psrhttpmessageresponseinterface).
940+
If your request handler function returns an invalid value or throws an
941+
unhandled `Exception` or `Throwable`, the server will automatically send a `500`
942+
(Internal Server Error) HTTP error response to the client.
943+
On top of this, it will emit an `error` event that can be used for logging
944+
purposes like this:
945+
946+
```php
947+
$server->on('error', function (Exception $e) {
948+
echo 'Error: ' . $e->getMessage() . PHP_EOL;
949+
if ($e->getPrevious() !== null) {
950+
echo 'Previous: ' . $e->getPrevious()->getMessage() . PHP_EOL;
951+
}
952+
});
953+
```
954+
955+
Note that the server will also emit an `error` event if the client sends an
956+
invalid HTTP request that never reaches your request handler function. See
957+
also [invalid request](#invalid-request) for more details.
958+
Additionally, a [streaming request](#streaming-request) body can also emit
959+
an `error` event on the request body.
960+
961+
The server will only send a very generic `500` (Interval Server Error) HTTP
962+
error response without any further details to the client if an unhandled
963+
error occurs. While we understand this might make initial debugging harder,
964+
it also means that the server does not leak any application details or stack
965+
traces to the outside by default. It is usually recommended to catch any
966+
`Exception` or `Throwable` within your request handler function or alternatively
967+
use a [`middleware`](#middleware) to avoid this generic error handling and
968+
create your own HTTP response message instead.
837969

838970
#### Default response headers
839971

0 commit comments

Comments
 (0)