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}