Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

Allow HEAD and OPTIONS requests for any matched route #413

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions composer.json
Expand Up @@ -20,7 +20,7 @@
"container-interop/container-interop": "^1.1",
"psr/http-message": "^1.0",
"zendframework/zend-diactoros": "^1.1",
"zendframework/zend-expressive-router": "^1.1",
"zendframework/zend-expressive-router": "^1.3.2",
"zendframework/zend-expressive-template": "^1.0.1",
"zendframework/zend-stratigility": "^1.3.1",
"fig/http-message-util": "^1.1"
Expand All @@ -31,9 +31,9 @@
"mockery/mockery": "^0.9.5",
"phpunit/phpunit": "^5.0",
"zendframework/zend-coding-standard": "~1.0.0",
"zendframework/zend-expressive-aurarouter": "^1.0",
"zendframework/zend-expressive-fastroute": "^1.0",
"zendframework/zend-expressive-zendrouter": "^1.0",
"zendframework/zend-expressive-aurarouter": "^1.1.3",
"zendframework/zend-expressive-fastroute": "^1.3",
"zendframework/zend-expressive-zendrouter": "^1.3",
"zendframework/zend-servicemanager": "^2.6 || ^3.1.1"
},
"autoload": {
Expand Down
192 changes: 97 additions & 95 deletions composer.lock

Large diffs are not rendered by default.

133 changes: 133 additions & 0 deletions doc/book/features/middleware/implicit-methods-middleware.md
@@ -0,0 +1,133 @@
# ImplicitHeadMiddleware and ImplicitOptionsMiddleware

Starting with version 1.1, Expressive offers middleware for implicitly
supporting `HEAD` and `OPTIONS` requests. The HTTP/1.1 specifications indicate
that all server implementations _must_ support `HEAD` requests for any given
URI, and that they _should_ support `OPTIONS` requests. To make this possible,
we have added features to our routing layer, and middleware that can detect
_implicit_ support for these methods (i.e., the route was not registered
_explicitly_ with the method).

## ImplicitHeadMiddleware

`Zend\Expressive\Middleware\ImplicitHeadMiddleware` provides support for
handling `HEAD` requests to routed middleware when the route does not expliclity
allow for the method. It should be registered _between_ the routing and dispatch
middleware.

By default, it can be instantiated with no extra arguments. However, you _may_
provide a response instance to use by default to the constructor if you need to
craft special headers, status code, etc.

Register the dependency via `dependencies` configuration:

```php
use Zend\Expressive\Middleware\ImplicitHeadMiddleware;

return [
'dependencies' => [
'invokables' => [
ImplicitHeadMiddleware::class => ImplicitHeadMiddleware::class,
],

// or, if you have defined a factory to inject a response:
'factories' => [
ImplicitHeadMiddleware::class => \Your\ImplicitHeadMiddlewareFactory::class,
],
],
];
```

Within your application pipeline, add the middleware between the routing and
dispatch middleware:

```php
$app->pipeRoutingMiddleware();
$app->pipe(ImplicitHeadMiddleware::class);
// ...
$app->pipeDispatchMiddleware();
```

(Note: if you used the `expressive-pipeline-from-config` tool to create your
programmatic pipeline, or if you used the Expressive 1.1 skeleton or later, this
middleware is likely already in your pipeline, as is a dependency entry.)

When in place, it will do the following:

- If the request method is `HEAD`, AND
- the request composes a `RouteResult` attribute, AND
- the route result composes a `Route` instance, AND
- the route returns true for the `implicitHead()` method, THEN
- the middleware will return a response.

In all other cases, it returns the result of delegating to the next middleware
layer.

When `implicitHead()` is matched, one of two things may occur. First, if the
route does not support the `GET` method, then the middleware returns the
composed response (either the one injected at instantiation, or an empty
instance). However, if `GET` is supported, it will dispatch the next layer, but
with a `GET` request instead of `HEAD`; additionally, it will inject the
returned response with an empty response body before returning it.

## ImplicitOptionsMiddleware

`Zend\Expressive\Middleware\ImplicitOptionsMiddleware` provides support for
handling `OPTIONS` requests to routed middleware when the route does not
expliclity allow for the method. Like the `ImplicitHeadMiddleware`, it should be
registered _between_ the routing and dispatch middleware.

By default, it can be instantiated with no extra arguments. However, you _may_
provide a response prototype instance to use by default to the constructor if
you need to craft special headers, status code, etc.

Register the dependency via `dependencies` configuration:

```php
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;

return [
'dependencies' => [
'invokables' => [
ImplicitOptionsMiddleware::class => ImplicitOptionsMiddleware::class,
],

// or, if you have defined a factory to inject a response:
'factories' => [
ImplicitOptionsMiddleware::class => \Your\ImplicitOptionsMiddlewareFactory::class,
],
],
];
```

Within your application pipeline, add the middleware between the routing and
dispatch middleware:

```php
$app->pipeRoutingMiddleware();
$app->pipe(ImplicitOptionsMiddleware::class);
// ...
$app->pipeDispatchMiddleware();
```

(Note: if you used the `expressive-pipeline-from-config` tool to create your
programmatic pipeline, or if you used the Expressive 1.1 skeleton or later, this
middleware is likely already in your pipeline, as is a dependency entry.)

When in place, it will do the following:

- If the request method is `OPTIONS`, AND
- the request composes a `RouteResult` attribute, AND
- the route result composes a `Route` instance, AND
- the route returns true for the `implicitOptions()` method, THEN
- the middleware will return a response with an `Allow` header indicating
methods the route allows.

In all other cases, it returns the result of delegating to the next middleware
layer.

One thing to note: the allowed methods reported by the route and/or route
result, and returned via the `Allow` header, may vary based on router
implementation. In most cases, it should be an aggregate of all routes using the
same path specification; however, it *could* be only the methods supported
explicitly by the matched route.
41 changes: 41 additions & 0 deletions doc/book/reference/migration/to-v1.1.md
Expand Up @@ -8,6 +8,7 @@ you should be aware of, and potentially update your application to adopt:
- Error handling
- Programmatic middleware pipelines
- Usage of [http-interop middleware](https://github.com/http-interop/http-middleware)
- Implicit handling of `HEAD` and `OPTIONS` requests

## Original messages

Expand Down Expand Up @@ -747,3 +748,43 @@ get full usage information.

Use this tool to identify potential problem areas in your application, and
update your code to use the new error handling facilities as outlined above.

## Handling HEAD and OPTIONS requests

Prior to 1.1, it was possible to route middleware that could not handle `HEAD`
and/or `OPTIONS` requests. Per [RFC 7231, section 4.1](https://tools.ietf.org/html/rfc7231#section-4.1),
"all general-purpose servers MUST support the methods GET and HEAD. All other
methods are OPTIONAL." Additionally, most servers and implementors agree that
`OPTIONS` _should_ be supported for any given resource, so that consumers can
determine what methods are allowed for the given resource.

To make this happen, the Expressive project implemented several features.

First, zend-expressive-router 1.3.0 introduced several features in both
`Zend\Expressive\Router\Route` and `Zend\Expressive\Router\RouteResult` to help
consumers implement support for `HEAD` and `OPTIONS` in an automated way. The
`Route` class now has two new methods, `implicitHead()` and `implicitOptions()`;
these each return a boolean `true` value if support for those methods is
_implicit_ — i.e., not defined explicitly for the route. The `RouteResult`
class now introduces a new factory method, `fromRoute()`, that will create an
instance from a `Route` instance; this then allows consumers of a `RouteResult`
to query the `Route` to see if a matched `HEAD` or `OPTIONS` request needs
automated handling. Each of the supported router implementations were updated to
use this method, as well as to return a successful routing result if `HEAD`
and/or `OPTIONS` requests are submitted, but the route does not explicitly
support the method.

Within Expressive itself, we now offer two new middleware to provide this
automation:

- `Zend\Expressive\Middleware\ImplicitHeadMiddleware`
- `Zend\Expressive\Middleware\ImplicitOptionsMiddleware`

If you want to support these methods automatically, each of these should be
enabled between the routing and dispatch middleware. If you use the
`expressive-pipeline-from-config` tool as documented in the
[programmatic pipeline migration section](#migrate-to-programmatic-pipelines),
entries for each will be injected into your generated pipeline.

Please see the [chapter on the implicit methods middleware](../../features/middleware/implicit-methods-middleware.md)
for more information on each.
2 changes: 2 additions & 0 deletions mkdocs.yml
Expand Up @@ -31,6 +31,8 @@ pages:
- 'Using Twig': features/template/twig.md
- 'Using zend-view': features/template/zend-view.md
- 'Error Handling': features/error-handling.md
- Middleware:
- 'Implicit HEAD and OPTIONS Middleware': features/middleware/implicit-methods-middleware.md
- Helpers:
- Introduction: features/helpers/intro.md
- UrlHelper: features/helpers/url-helper.md
Expand Down
111 changes: 111 additions & 0 deletions src/Middleware/ImplicitHeadMiddleware.php
@@ -0,0 +1,111 @@
<?php
/**
* @see https://github.com/zendframework/zend-expressive for the canonical source repository
* @copyright Copyright (c) 2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Expressive\Middleware;

use Fig\Http\Message\RequestMethodInterface as RequestMethod;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
use Zend\Expressive\Router\RouteResult;

/**
* Handle implicit HEAD requests.
*
* Place this middleware after the routing middleware so that it can handle
* implicit HEAD requests -- requests where HEAD is used, but the route does
* not explicitly handle that request method.
*
* When invoked, it will create an empty response with status code 200.
*
* You may optionally pass a response prototype to the constructor; when
* present, that instance will be returned instead.
*
* The middleware is only invoked in these specific conditions:
*
* - a HEAD request
* - with a `RouteResult` present
* - where the `RouteResult` contains a `Route` instance
* - and the `Route` instance defines implicit HEAD.
*
* In all other circumstances, it will return the result of the delegate.
*
* If the route instance supports GET requests, the middleware dispatches
* the next layer, but alters the request passed to use the GET method;
* it then provides an empty response body to the returned response.
*/
class ImplicitHeadMiddleware
{
/**
* @var null|ResponseInterface
*/
private $response;

/**
* @param null|ResponseInterface $response Response prototype to return
* for implicit HEAD requests; if none provided, an empty zend-diactoros
* instance will be created.
*/
public function __construct(ResponseInterface $response = null)
{
$this->response = $response;
}

/**
* Handle an implicit HEAD request.
*
* If the route allows GET requests, dispatches as a GET request and
* resets the response body to be empty; otherwise, creates a new empty
* response.
*
* @param ServerRequestInterface $request
* @param ResponseInterface $response
* @param callable $next
* @return ResponseInterface
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
if ($request->getMethod() !== RequestMethod::METHOD_HEAD) {
return $next($request, $response);
}

if (false === ($result = $request->getAttribute(RouteResult::class, false))) {
return $next($request, $response);
}

$route = $result->getMatchedRoute();
if (! $route || ! $route->implicitHead()) {
return $next($request, $response);
}

if (! $route->allowsMethod(RequestMethod::METHOD_GET)) {
return $this->getResponse();
}

$response = $next(
$request->withMethod(RequestMethod::METHOD_GET),
$response
);

return $response->withBody(new Stream('php://temp/', 'wb+'));
}

/**
* Return the response prototype to use for an implicit HEAD request.
*
* @return ResponseInterface
*/
private function getResponse()
{
if ($this->response) {
return $this->response;
Copy link
Member

Choose a reason for hiding this comment

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

Why not check and assign it on __contructor?

Copy link
Member Author

Choose a reason for hiding this comment

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

There's no need to create one if the middleware does not need to return a response in the first place.

}

return new Response();
}
}