|
1 |
| -# HTTP Middleware |
| 1 | +HTTP Server Middleware |
| 2 | +====================== |
2 | 3 |
|
3 |
| -PSR-15 interfaces for HTTP middleware. |
| 4 | +1. Summary |
| 5 | +---------- |
| 6 | + |
| 7 | +The purpose of this PSR is to provide an interface that defines the formal |
| 8 | +method signature for HTTP Server Middleware that is compatible with HTTP |
| 9 | +Messages, as defined in [PSR-7][psr7]. |
| 10 | + |
| 11 | +[psr7]: http://www.php-fig.org/psr/psr-7/ |
| 12 | + |
| 13 | +2. Why Bother? |
| 14 | +-------------- |
| 15 | + |
| 16 | +The HTTP Messages specification does not contain any reference to HTTP Middleware. |
| 17 | + |
| 18 | +The design pattern used by middleware has existed for many years as the |
| 19 | +[pipeline pattern][pipeline], or more specifically, "linear pipeline processing". |
| 20 | +The general concept of reusable middleware was popularized within PHP by |
| 21 | +[StackPHP][stackphp]. Since the release of the HTTP Messages standard, a number |
| 22 | +of frameworks have adopted middleware that use HTTP Message interfaces. |
| 23 | + |
| 24 | +Agreeing on a formal server middleware interface eliminates several problems and |
| 25 | +provides a number of benefits: |
| 26 | + |
| 27 | +* Provides a formal standard for middleware developers to commit to. |
| 28 | +* Eliminates duplication of similar interfaces defined by various frameworks. |
| 29 | +* Avoids minor discrepancies in method signatures. |
| 30 | +* Enables any middleware component to run in any compatible framework. |
| 31 | + |
| 32 | +[pipeline]: https://en.wikipedia.org/wiki/Pipeline_(computing) |
| 33 | +[stackphp]: http://stackphp.com/ |
| 34 | + |
| 35 | +3. Scope |
| 36 | +-------- |
| 37 | + |
| 38 | +### 3.1 Goals |
| 39 | + |
| 40 | +* Create a middleware interface that uses HTTP Messages. |
| 41 | +* Ensure that middleware will not be coupled to a specific implementation of HTTP Messages. |
| 42 | +* Implement a middleware signature that is based on best practices. |
| 43 | + |
| 44 | +### 3.2 Non-Goals |
| 45 | + |
| 46 | +* Attempting to define how middleware is dispatched. |
| 47 | +* Attempting to define interfaces for client/asynchronous middleware. |
| 48 | +* Attempting to define the mechanism by which HTTP responses are created. |
| 49 | + |
| 50 | +4. Approaches |
| 51 | +------------- |
| 52 | + |
| 53 | +There are currently two common approaches to server middleware that use HTTP Messages. |
| 54 | + |
| 55 | +### 4.1 Double Pass |
| 56 | + |
| 57 | +The signature used by most middleware implementations has been mostly the same |
| 58 | +and is based on [Express middleware][express], which is defined as: |
| 59 | + |
| 60 | +``` |
| 61 | +fn(request, response, next): response |
| 62 | +``` |
| 63 | + |
| 64 | +[express]: http://expressjs.com/en/guide/writing-middleware.html |
| 65 | + |
| 66 | +Based on the middleware implementations already used by frameworks that have |
| 67 | +adopted this signature, the following commonalities are observed: |
| 68 | + |
| 69 | +* The middleware is defined as a [callable][php-callable]. |
| 70 | +* The middleware is passed 3 arguments during invocation: |
| 71 | + 1. A `ServerRequestInterface` implementation. |
| 72 | + 2. A `ResponseInterface` implementation. |
| 73 | + 3. A `callable` that receives the request and response to delegate the next middleware. |
| 74 | + |
| 75 | +[php-callable]: http://php.net/manual/language.types.callable.php |
| 76 | + |
| 77 | +A significant number of projects provide and/or use exactly the same interface. |
| 78 | +This approach is often referred to as "double pass" in reference to both the |
| 79 | +request and response being passed to the middleware. |
| 80 | + |
| 81 | +#### 4.1.1 Projects Using Double Pass |
| 82 | + |
| 83 | +* [mindplay/middleman](https://github.com/mindplay-dk/middleman/blob/1.0.0/src/MiddlewareInterface.php#L24) |
| 84 | +* [relay/relay](https://github.com/relayphp/Relay.Relay/blob/1.0.0/src/MiddlewareInterface.php#L24) |
| 85 | +* [slim/slim](https://github.com/slimphp/Slim/blob/3.4.0/Slim/MiddlewareAwareTrait.php#L66-L75) |
| 86 | +* [zendframework/zend-stratigility](https://github.com/zendframework/zend-stratigility/blob/1.0.0/src/MiddlewarePipe.php#L69-L79) |
| 87 | + |
| 88 | +#### 4.1.2 Middleware Implementing Double Pass |
| 89 | + |
| 90 | +* [bitexpert/adroit](https://github.com/bitExpert/adroit) |
| 91 | +* [akrabat/rka-ip-address-middleware](https://github.com/akrabat/rka-ip-address-middleware) |
| 92 | +* [akrabat/rka-scheme-and-host-detection-middleware](https://github.com/akrabat/rka-scheme-and-host-detection-middleware) |
| 93 | +* [bear/middleware](https://github.com/bearsunday/BEAR.Middleware) |
| 94 | +* [hannesvdvreken/psr7-middlewares](https://github.com/hannesvdvreken/psr7-middlewares) |
| 95 | +* [los/api-problem](https://github.com/Lansoweb/api-problem) |
| 96 | +* [los/los-rate-limit](https://github.com/Lansoweb/LosRateLimit) |
| 97 | +* [monii/monii-action-handler-psr7-middleware](https://github.com/monii/monii-action-handler-psr7-middleware) |
| 98 | +* [monii/monii-nikic-fast-route-psr7-middleware](https://github.com/monii/monii-nikic-fast-route-psr7-middleware) |
| 99 | +* [monii/monii-response-assertion-psr7-middleware](https://github.com/monii/monii-response-assertion-psr7-middleware) |
| 100 | +* [mtymek/blast-base-url](https://github.com/mtymek/blast-base-url) |
| 101 | +* [ocramius/psr7-session](https://github.com/Ocramius/PSR7Session) |
| 102 | +* [oscarotero/psr7-middlewares](https://github.com/oscarotero/psr7-middlewares) |
| 103 | +* [php-middleware/block-robots](https://github.com/php-middleware/block-robots) |
| 104 | +* [php-middleware/http-authentication](https://github.com/php-middleware/http-authentication) |
| 105 | +* [php-middleware/log-http-messages](https://github.com/php-middleware/log-http-messages) |
| 106 | +* [php-middleware/maintenance](https://github.com/php-middleware/maintenance) |
| 107 | +* [php-middleware/phpdebugbar](https://github.com/php-middleware/phpdebugbar) |
| 108 | +* [php-middleware/request-id](https://github.com/php-middleware/request-id) |
| 109 | +* [relay/middleware](https://github.com/relayphp/Relay.Middleware) |
| 110 | + |
| 111 | +The primary downside of this interface is that the while the interface itself is |
| 112 | +a callable, there is currently no way to type hint a closure in a similar way. |
| 113 | +This may be resolved in the future with [functional interfaces][php-functional]. |
| 114 | + |
| 115 | +[php-functional]: https://wiki.php.net/rfc/functional-interfaces |
| 116 | + |
| 117 | +### 4.2 Single Pass (Lambda) |
| 118 | + |
| 119 | +The other approach to middleware is much closer to [StackPHP][stackphp] style |
| 120 | +and is defined as: |
| 121 | + |
| 122 | +``` |
| 123 | +fn(request, frame): response |
| 124 | +``` |
| 125 | + |
| 126 | +Middleware taking this approach generally has the following commonalities: |
| 127 | + |
| 128 | +* The middleware is defined with a specific interface with a method that takes |
| 129 | + the request for processing. |
| 130 | +* The middleware is passed 2 arguments during invocation: |
| 131 | + 1. An object that represents an HTTP request. |
| 132 | + 2. A delegate that receives the request to dispatch next middleware in the pipeline. |
| 133 | + |
| 134 | +In this form, middleware has no access to a response until one is generated by |
| 135 | +innermost middleware. Middleware can then modify the response before returning |
| 136 | +back up the stack. |
| 137 | + |
| 138 | +This approach is often referred to as "single pass" or "lambda" in reference to |
| 139 | +only the request being passed to the middleware. |
| 140 | + |
| 141 | +[php-closure]: http://php.net/closure |
| 142 | + |
| 143 | +#### 4.2.1 Projects Using Single Pass |
| 144 | + |
| 145 | +There are fewer examples of this approach within projects using HTTP Messages, |
| 146 | +with one notable exception. |
| 147 | + |
| 148 | +[Guzzle middleware][guzzle-middleware] is focused on outgoing (client) requests |
| 149 | +and uses this signature: |
| 150 | + |
| 151 | +``` |
| 152 | +function(RequestInterface $request, array $options): ResponseInterface |
| 153 | +``` |
| 154 | + |
| 155 | +#### 4.2.2 Additional Projects Using Single Pass |
| 156 | + |
| 157 | +There are also significant projects that predate HTTP Messages using this approach. |
| 158 | + |
| 159 | +[StackPHP][stackphp] is based on [Symfony HttpKernel][httpkernel] and supports |
| 160 | +middleware with this signature: |
| 161 | + |
| 162 | +``` |
| 163 | +handle(Request $request, $type, $catch): Response |
| 164 | +``` |
| 165 | + |
| 166 | +*__Note__: While Stack has multiple arguments, a response object is not included.* |
| 167 | + |
| 168 | +[Laravel middleware][laravel-middleware] uses Symfony components and supports |
| 169 | +middleware with this signature: |
| 170 | + |
| 171 | +``` |
| 172 | +function handle(Request $request, callable $next): Response |
| 173 | +``` |
| 174 | + |
| 175 | +[guzzle-middleware]: http://docs.guzzlephp.org/en/latest/handlers-and-middleware.html |
| 176 | +[httpkernel]: https://symfony.com/doc/2.0/components/http_kernel/introduction.html |
| 177 | +[laravel-middleware]: https://laravel.com/docs/master/middleware |
| 178 | + |
| 179 | +### 4.3 Comparison of Approaches |
| 180 | + |
| 181 | +The single pass approach to middleware has been well established in the PHP |
| 182 | +community for many years. This is most evident with the large number of packages |
| 183 | +that are based around StackPHP. |
| 184 | + |
| 185 | +The double pass approach is much newer but has been almost universally used by |
| 186 | +early adopters of HTTP Messages. |
| 187 | + |
| 188 | +### 4.4 Chosen Approach |
| 189 | + |
| 190 | +Despite the nearly universal adoption of the double-pass approach there are |
| 191 | +significant issues regarding implementation. |
| 192 | + |
| 193 | +The most severe is that passing an empty response has no guarantees that the |
| 194 | +response is in a usable state. This is further exacerbated by the fact that a |
| 195 | +middleware may modify the response before passing it for further dispatching. |
| 196 | + |
| 197 | +Further compounding the problem is that there is no way to ensure that the |
| 198 | +response body has not been written to, which can lead to incomplete output or |
| 199 | +error responses being sent with cache headers attached. It is also possible |
| 200 | +to end up with [corrupted body content][rob-allen-filtering] when writing over |
| 201 | +existing body content if the new content is shorter than the original. The most |
| 202 | +effective way to resolve these issues is to always provide a fresh stream when |
| 203 | +modifying the body of a message. |
| 204 | + |
| 205 | +[rob-allen-filtering]: https://akrabat.com/filtering-the-psr-7-body-in-middleware/ |
| 206 | + |
| 207 | +Some have argued that passing the response helps ensure dependency inversion. |
| 208 | +While it is true that it helps avoid depending on a specific implementation of |
| 209 | +HTTP messages, the problem can also be resolved by injecting factories into the |
| 210 | +middleware to create HTTP message objects, or by injecting empty message instances. |
| 211 | +With the creation of HTTP Factories in [PSR-17][psr17], a standard approach to |
| 212 | +handling dependency inversion is possible. |
| 213 | + |
| 214 | +[psr17]: https://github.com/php-fig/fig-standards/blob/master/proposed/http-factory/http-factory-meta.md |
| 215 | + |
| 216 | +A more subjective, but also important, concern is that existing double-pass |
| 217 | +middleware typically uses the `callable` type hint to refer to middleware. |
| 218 | +This makes strict typing impossible, as there is no assurance that the `callable` |
| 219 | +being passed implements a middleware signature, which reduces runtime safety. |
| 220 | + |
| 221 | +**Due to these significant issues the lambda approach has been choosen for this proposal.** |
| 222 | + |
| 223 | +5. Design Decisions |
| 224 | +------------------- |
| 225 | + |
| 226 | +### 5.1 Middleware Design |
| 227 | + |
| 228 | +The `ServerMiddlewareInterface` defines a single method that accepts a server |
| 229 | +request and a delegate and must return a response. The middleware may: |
| 230 | + |
| 231 | +- Evolve the request before passing it to the delegate to execute the next |
| 232 | + available middleware. |
| 233 | +- Evolve the response received from the delegate before returning it. |
| 234 | +- Create and return a response without passing it to the delegate, thereby |
| 235 | + preventing any further middleware from executing. |
| 236 | + |
| 237 | +#### Why doesn't middleware use `__invoke`? |
| 238 | + |
| 239 | +Doing so would conflict with existing middleware that implements the double-pass |
| 240 | +approach and may want to implement the middleware interface. |
| 241 | + |
| 242 | +In addition, classes that define `__invoke` can be type hinted as `callable`, |
| 243 | +which results in less strict typing. This is generally undesirable, especially |
| 244 | +when the `__invoke` method uses strict typing. |
| 245 | + |
| 246 | +#### Why is a server request required? |
| 247 | + |
| 248 | +To make it clear that the middleware can only be used in a synchronous, server |
| 249 | +side context. |
| 250 | + |
| 251 | +While not all middleware will need to use the additional methods defined by the |
| 252 | +server request interface, external requests are typically handled asynchronously |
| 253 | +and would need to return a [promise][promises] of a response. (This is primarily |
| 254 | +due to the fact that multiple requests can be made in parallel and processed as |
| 255 | +they are returned.) It is outside the scope of this proposal to address the needs |
| 256 | +of asynchronous request/response life cycle. |
| 257 | + |
| 258 | +Attempting to define client middleware would be premature at this point. Any future |
| 259 | +proposal that is focused on client side request processing should have the opportunity |
| 260 | +to define a standard that is specific to the nature of asynchronous middleware. |
| 261 | + |
| 262 | +_See "client vs server side middleware" in [relevant links](#relevant-links) for |
| 263 | +additional information._ |
| 264 | + |
| 265 | +[promises]: https://promisesaplus.com/ |
| 266 | + |
| 267 | +### 5.2 Delegate Design |
| 268 | + |
| 269 | +The `DelegateInterface` defines a single method that accepts a request and |
| 270 | +returns a response. The delegate interface must be implemented by any middleware |
| 271 | +dispatcher that uses middleware implementing `ServerMiddlewareInterface`. |
| 272 | + |
| 273 | +#### Why isn't the delegate a `callable`? |
| 274 | + |
| 275 | +Using an interface type hint improves runtime safety and IDE support. |
| 276 | + |
| 277 | +In addition, it allows compatibility with existing middleware that already defines |
| 278 | +an `__invoke` method. |
| 279 | + |
| 280 | +_See "discussion of FrameInterface" in [relevant links](#relevant-links) for |
| 281 | +additional information._ |
| 282 | + |
| 283 | +#### Why does the delegate conflict with middleware? |
| 284 | + |
| 285 | +Both the middleware and delegate interface define a `process` method to prevent |
| 286 | +misuse of middleware as delegates. |
| 287 | + |
| 288 | +If a middleware was used as a delegate the entire middleware stack would end up |
| 289 | +recursive, instead of piped. The implementation of delegate should be defined |
| 290 | +within middleware dispatching systems. |
| 291 | + |
| 292 | +6. People |
| 293 | +--------- |
| 294 | + |
| 295 | +### 6.1 Editor(s) |
| 296 | + |
| 297 | +* Woody Gilk, <woody.gilk@gmail.com> |
| 298 | + |
| 299 | +### 6.2 Sponsors |
| 300 | + |
| 301 | +* Paul M Jones, <pmjones88@gmail.com> (Coordinator) |
| 302 | +* Jason Coward, <jason@opengeek.com> (Sponsor) |
| 303 | + |
| 304 | +### 6.3 Contributors |
| 305 | + |
| 306 | +* Rasmus Schultz, <rasmus@mindplay.dk> |
| 307 | +* Matthew Weier O'Phinney, <mweierophinney@gmail.com> |
| 308 | + |
| 309 | +7. Votes |
| 310 | +-------- |
| 311 | + |
| 312 | +* [Entrance Vote](https://groups.google.com/d/msg/php-fig/v9AijALWJhI/04XCwqgIEAAJ) |
| 313 | +* **Acceptance Vote:** _(not yet taken)_ |
| 314 | + |
| 315 | +8. Relevant Links |
| 316 | +----------------- |
| 317 | + |
| 318 | +_**Note:** Order descending chronologically._ |
| 319 | + |
| 320 | +* [PHP-FIG mailing list thread](https://groups.google.com/d/msg/php-fig/vTtGxdIuBX8/NXKieN9vDQAJ) |
| 321 | +* [The PHP League middleware proposal](https://groups.google.com/d/msg/thephpleague/jyztj-Nz_rw/I4lHVFigAAAJ) |
| 322 | +* [PHP-FIG discussion of FrameInterface](https://groups.google.com/d/msg/php-fig/V12AAcT_SxE/aRXmNnIVCwAJ) |
| 323 | +* [PHP-FIG discussion about client vs server side middleware](https://groups.google.com/d/msg/php-fig/vBk0BRgDe2s/GTaT0yKNBgAJ) |
| 324 | + |
| 325 | +9. Errata |
| 326 | +--------- |
| 327 | + |
| 328 | +... |
0 commit comments