8000 Middleware by WyriHaximus · Pull Request #213 · reactphp/http · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Middleware #213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 23 commits into from
Closed

Middleware #213

wants to merge 23 commits into from

Conversation

WyriHaximus
Copy link
Member
@WyriHaximus WyriHaximus commented Aug 19, 2017

During DPC @clue and I had a long chat about how to handle body parsers without loosing the current flexibility. We came to the conclusion that middlewares are the way to go. The following proposal is inspired by the WIP PSR-15 but doesn't implement it (I'll get back on that later in this PR).


Suggested reading order

This PR contains a lot of changes, how ever most of those changes are in examples and tests. Here is a recommended reading order:

  1. README.md
  2. MiddlewareInterface.php
  3. MiddlewareStackInterface.php
  4. MiddlewareStack.php
  5. Server.php
  6. Middleware/Callback.php
  7. Middleware/LimitHandlers.php
  8. Middleware/Buffer.php
  9. The rest

Major changes

Where 0.7 only requires you to pass a callable to handle incoming requests, this PR proposes to use middleware for that. (While still leaving the callable way intact by magically wrapping it.) One way to setup middleware by passing an array with middlewares implementing MiddlewareInterface:

$server = new Server([
    new Buffer(),
    new Callback(function ($request) {
        return new Response(200);
    }),
]);

Or by passing in a concrete implementation of MiddlewareStackInterface:

$server = new Server(new MiddlewareStack([
    new Buffer(),
    new Callback(function (ServerRequestInterface $request) {
        return new Response(200);
    }),
]));

The latter is done automatically when doing the former internally in the server. But you can create your own middleware stack implementation and use that instead.

Included middlewares

This PR includes three middlewares Buffer, Callback, and LimitHandlers.

Buffer

Buffers the request body until it is fully in or when it reach the given size:

$middlewares = [
    new Buffer(),
];

Callback

Handles request just like 0.7 using a callback:

$middlewares = [
    new Callback(function (ServerRequestInterface $request) {
        return new Response(200);
    }),
];

LimitHandlers

Limits the number of concurrent request being handled at the same time:

$middlewares = [
    new LimitHandlers(10),
];

Body parsing

Currently one of our main issues with body parsing is the memory usage. But with the LimitHandlers we can pause the incoming request stream until we're ready handle it thus limiting the amount of memory needed to handle request. And with Buffer we can first stream the body in, parse it with for example BodyParser, and then hand it to the next middleware on the stack.

PSR-15

While the middleware implementation in this PR is inspired by the work done for the coming PSR-15 it doesn't implement it due to a small but major different. This implementation relies on promises where PSR-15 always assumes a response to be returned. An adapter for PSR-15 is beyond the scope of this PR but not for a Friends of ReactPHP packages and thus I've 8000 created for/http-middleware-psr15-adapter that utilizes RecoilPHP and on the fly PHP parsing and rewriting of PSR-15 middlewares.

With that package you can wrap the adapter around a PSR-15 as follows:

$middlewares = [
    new PSR15Middleware(
        $loop, 
        'Namespace\MiddlewareClassname', 
        [/** Constructor arguments */]
    ),
];

*Note: I've added last section to show how easy it can be to add PSR-15 middlewares, with the rewriting there is no guarantee for success

@WyriHaximus
Copy link
Member Author

Ping: @clue @jsor @andig @marcj

@andig
Copy link
Contributor
andig commented Aug 19, 2017

Would it make sense- in terms of backwards compatibility- to allow the server to apply a bit of magic? If the parameter is a callable, wrap it in a Callback. If it's not an array, wrap it in an array? This would also allow for a bit less code and indents for something like the 95% use case where you might only want your callable to be called without limiting or similar?

@WyriHaximus
Copy link
Member Author

@andig that does make a lot of sense to me, I'll update the PR with that later today 👍


namespace React\Http;

use Interop\Http\ServerMiddleware\MiddlewareInterface;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Must be React\Http\MiddlewareInterface.

* @param ServerRequestInterface $request
* @param MiddlewareStackInterface $stack
*
* @return PromiseInterface<ResponseInterface>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can also return a response directly: ResponseInterface|PromiseInterface<ResponseInterface>

src/Server.php Outdated
}

$this->callback = $callback;
throw new \InvalidArgumentException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception message missing explaining what types of arguments can be given to the constructor.

src/Server.php Outdated
}

$this->callback = $callback;
throw new \InvalidArgumentException('Only MiddlewareInterface[] or MiddlewareStackInterface implementations are supported');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would not it be better to use fully qualified names?

@WyriHaximus
Copy link
Member Author

@andig, @mdrost, and @jsor I've updated the PR with your suggestions and comments

@andig
Copy link
Contributor
andig commented Aug 19, 2017

Passing a Callback without array is not supported yet. But maybe that case isn‘t that important after all. I really like the original, simple callable stile. Thank you!

@WyriHaximus
Copy link
Member Author

@andig it is, I've reverted that change and you can now pass in a callable, an array with middlewares or a middleware stack to handle your request.

src/Server.php Outdated
$promise = new Promise(function ($resolve, $reject) use ($callback, $request, &$cancel) {
$cancel = $callback($request);
$promise = new Promise\Promise(function ($resolve, $reject) use ($middlewareStack, $request, &$cancel) {
$cancel = $middlewareStack->process($request);
$resolve($cancel);
});
Copy link
Member
@jsor jsor Aug 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since MiddlewareStack::process() always returns a promise, this can be simplified to just

$cancel = $middlewareStack->process($request);

Note, that catching exceptions should be moved to the MiddlewareStack, see #213 (comment)

)
)
);
}
Copy link
Member
@jsor jsor Aug 21, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To prevent exceptions thrown from process() bubbling out from here, this should be wrapped like this to turn them into rejections:

return new Promise\Promise(function ($resolve, $reject) use ($request, $middlewares, $middleware) {
    $response = $middleware->process(
        $request,
        new self(
            $this->defaultResponse,
            $middlewares
        )
    );

    $resolve($response);
});

@WyriHaximus
Copy link
Member Author

@jsor Updated with your suggestions, had a lot of fun with the cancellation forwarding

… runners/stacks) are callables. As a result the Callback middleware has been removed as it has become obsolete
src/Server.php Outdated
* );
* });
* $server = new Server([
* new Callback(function (ServerRequestInterface $request) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still using Callback removed in b4211aa

$middlewares = [
new LimitHandlers(5),
new Buffer(),
new Callback(function (ServerRequestInterface $request) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still using Callback removed in b4211aa

F438 README.md
return new Response(200);
}),
];
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leftover from b4211aa

$server = new Server(array(
new LimitHandlers(3), // Only handle three requests concurrently
new Buffer(), // Buffer the whole request body before proceeding
new Callback(function (ServerRequestInterface $request) use ($loop) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still using Callback removed in b4211aa

new Response(500),
array(
new Incre($counts),
new Callback(function (ServerRequestInterface $request) use ($loop) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still using Callback removed in b4211aa

@@ -2,6 +2,8 @@

namespace React\Tests\Http;

use React\Http\Middleware\Buffer;
use React\Http\Middleware\Callback;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused use's?

src/Server.php Outdated
$promise = new Promise(function ($resolve, $reject) use ($callback, $request, &$cancel) {
$cancel = $callback($request);
$promise = new Promise\Promise(function ($resolve, $reject) use ($request, &$cancel) {
$callable = $this->callable;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$this isn't available in PHP 5.3, see https://travis-ci.org/reactphp/http/jobs/271815811

$middlewares
)
);
$cancel->done($resolve, $reject);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since react/promise v1.x is supported, done() can't be used as its only available since v2.0. See https://travis-ci.org/reactphp/http/jobs/271815812

src/Server.php Outdated
$callable = $this->callable;
$cancel = $callable($request);
if ($cancel instanceof Promise\CancellablePromiseInterface) {
$cancel->done($resolve, $reject);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No done() because react/promise v1.x.

@WyriHaximus
Copy link
Member Author

Updated the PR (except documentation, examples and readme) turning all middlewares into invokables and also the MiddlewareStack. The idea behind it is that it doesn't require any code changes and thus is backwards compatible from the consumer end. Whilest still allowing us to add middleware

Ping @clue

@WyriHaximus
Copy link
Member Author

@jsor updated your points and the examples

$that = $this;
$cancel = null;
return new Promise\Promise(function ($resolve, $reject) use ($middleware, $request, $middlewares, &$cancel, $that) {
$cancel = $middleware(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're not working on an interface anymore (but on plain callables), there must be a check if $cancel is actually a promise (or not a Response).

@WyriHaximus
Copy link
Member Author

Extracted the Middleware Runner into #215

@WyriHaximus
Copy link
Member Author

Extracted the Buffer middleware into #216

@clue
Copy link
Member
clue commented Sep 7, 2017

Let me start with saying awesome! 🎉 It's really nice to see how our discussion and our very quick prototype at this year's DPC turned out in this PR!

I'd love to get this feature in ASAP, but this PR contains quite a few changes, so it's really hard to review in its current state. I see you've started splitting this into smaller PRs (#215 and #216), which I think makes perfect sense, so let's get this rolling! :shipit:


#### Callback

A few build in middlewares are shipped with this package, one of them is
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be built-in?


#### Buffer

Another build in middleware is `Buffer` which will buffer the incoming
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be built-in?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is superseded by smaller PR's split off, eg. #216 (which is already merged). This PR should probably closed by @WyriHaximus to avoid confusion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

@andig
Copy link
Contributor
andig commented Oct 3, 2017

@WyriHaximus thanks for all your efforts. Looking at react/http from consumer side I've been wondering why you're not using PSR-15?

@WyriHaximus
Copy link
Member Author

@andig One of the reasons is that PSR-15 requires a response from the middleware where we return either a response or a promise (we wrap the response in a promise anyway). Also going with callable's opens up a lot more flexibility letting you use closures just as easily as [$class, 'method'] or $class that has a __invoke. In case you want PSR-15 I've crafted https://github.com/friends-of-reactphp/http-middleware-psr15-adapter so you can use PSR-15 middleware as react/http middleware.

@WyriHaximus
Copy link
Member Author

FYI here is an example of a project where I'm using that adapter:

    $middleware = [
        new PSR15Middleware($loop, Uuid::class),
        new PSR15Middleware($loop, ClientIp::class),
        new PSR15Middleware($loop, AccessLog::class, [$logger], function ($accessLog) {
            return $accessLog->context(function (ServerRequestInterface $request, ResponseInterface $response) {
                return [
                    'client-ip' => $request->getAttribute('client-ip'),
                    'request-id' => $request->getHeaderLine('X-Uuid'),
                    'response-time' =>$response->getHeaderLine('X-Response-Time'),
                ];
            });
        }),
        new PSR15Middleware($loop, ResponseTime::class),
        new WithHeadersMiddleware([
            'X-Powered-By' => 'wyrimaps.net-static-maps-server (' . $version . ')',
        ]),
        new WithRandomHeadersMiddleware(Horde::headers(), 1, 2),
        new RequestBodyBufferMiddleware(),
        new CustomRequestBodyParsers(),
        new ClearBodyMiddleware(),
        new PSR15Middleware($loop, Expires::class, [$expires]),
        new PSR15Middleware($loop, Redirect::class, [[
                '/' => 'https://wyrimaps.net/',
                '/favicon.ico' => 'https://wyrimaps.net/favicon.ico',
        ]]),
        new PSR15Middleware($loop, Robots::class, [false]),
        new PSR15Middleware($loop, FastRoute::class, [$dispatcher]),
        new PSR15Middleware($loop, RequestHandler::class),
    ];
    $server = new HttpServer(new MiddlewareRunner($middleware));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants
0