friendship ended with social-app. php is my new best friend
1<?php
2
3namespace React\Http\Middleware;
4
5use OverflowException;
6use Psr\Http\Message\ServerRequestInterface;
7use React\Http\Io\BufferedBody;
8use React\Http\Io\IniUtil;
9use React\Promise\Promise;
10use React\Stream\ReadableStreamInterface;
11
12final class RequestBodyBufferMiddleware
13{
14 private $sizeLimit;
15
16 /**
17 * @param int|string|null $sizeLimit Either an int with the max request body size
18 * in bytes or an ini like size string
19 * or null to use post_max_size from PHP's
20 * configuration. (Note that the value from
21 * the CLI configuration will be used.)
22 */
23 public function __construct($sizeLimit = null)
24 {
25 if ($sizeLimit === null) {
26 $sizeLimit = \ini_get('post_max_size');
27 }
28
29 $this->sizeLimit = IniUtil::iniSizeToBytes($sizeLimit);
30 }
31
32 public function __invoke(ServerRequestInterface $request, $next)
33 {
34 $body = $request->getBody();
35 $size = $body->getSize();
36
37 // happy path: skip if body is known to be empty (or is already buffered)
38 if ($size === 0 || !$body instanceof ReadableStreamInterface || !$body->isReadable()) {
39 // replace with empty body if body is streaming (or buffered size exceeds limit)
40 if ($body instanceof ReadableStreamInterface || $size > $this->sizeLimit) {
41 $request = $request->withBody(new BufferedBody(''));
42 }
43
44 return $next($request);
45 }
46
47 // request body of known size exceeding limit
48 $sizeLimit = $this->sizeLimit;
49 if ($size > $this->sizeLimit) {
50 $sizeLimit = 0;
51 }
52
53 /** @var ?\Closure $closer */
54 $closer = null;
55
56 return new Promise(function ($resolve, $reject) use ($body, &$closer, $sizeLimit, $request, $next) {
57 // buffer request body data in memory, discard but keep buffering if limit is reached
58 $buffer = '';
59 $bufferer = null;
60 $body->on('data', $bufferer = function ($data) use (&$buffer, $sizeLimit, $body, &$bufferer) {
61 $buffer .= $data;
62
63 // On buffer overflow keep the request body stream in,
64 // but ignore the contents and wait for the close event
65 // before passing the request on to the next middleware.
66 if (isset($buffer[$sizeLimit])) {
67 assert($bufferer instanceof \Closure);
68 $body->removeListener('data', $bufferer);
69 $bufferer = null;
70 $buffer = '';
71 }
72 });
73
74 // call $next with current buffer and resolve or reject with its results
75 $body->on('close', $closer = function () use (&$buffer, $request, $resolve, $reject, $next) {
76 try {
77 // resolve with result of next handler
78 $resolve($next($request->withBody(new BufferedBody($buffer))));
79 } catch (\Exception $e) {
80 $reject($e);
81 } catch (\Throwable $e) { // @codeCoverageIgnoreStart
82 // reject Errors just like Exceptions (PHP 7+)
83 $reject($e); // @codeCoverageIgnoreEnd
84 }
85 });
86
87 // reject buffering if body emits error
88 $body->on('error', function (\Exception $e) use ($reject, $body, $closer) {
89 // remove close handler to avoid resolving, then close and reject
90 assert($closer instanceof \Closure);
91 $body->removeListener('close', $closer);
92 $body->close();
93
94 $reject(new \RuntimeException(
95 'Error while buffering request body: ' . $e->getMessage(),
96 $e->getCode(),
97 $e
98 ));
99 });
100 }, function () use ($body, &$closer) {
101 // cancelled buffering: remove close handler to avoid resolving, then close and reject
102 assert($closer instanceof \Closure);
103 $body->removeListener('close', $closer);
104 $body->close();
105
106 throw new \RuntimeException('Cancelled buffering request body');
107 });
108 }
109}