werk365/etagconditionals

Laravel package to enable support for ETags and handling If-Match and If-None-Match conditional requests

1.3.0 2021-08-02 09:47 UTC

README

Latest Version on Packagist Total Downloads StyleCI Scrutinizer Code Quality Build Status

This package provides a set of middlewares to both set ETags and handle HTTP Conditional Requests.

Currently both If-None-Matched and If-Match are supported.

The package aims to provide the tools to provide better client-side caching when building an API with Laravel, as well as preventing mid-air collisions.

When using the package and enabling the middleware, your client (browser) will take care of handling the caching provided by the ETag and If-None-Match headers automatically.

Installation

Via Composer

$ composer require werk365/etagconditionals

Usage

You can either use the middleware group, automatically applying all available middleware (recommended if using an apiResource route for example), by setting the etag middleware, or apply the middlewares individually.

Currently available middleware:

  • setEtag
  • ifMatch
  • ifNoneMatch

setEtag

Method: Any

This middleware will set the ETag header on your responses. The ETag header is equal to a md5 hash of $response->getContent(). HEAD requests are supported by transforming the request to a GET request and changing it back on the response.

ifMatch

Method: PATCH

This middleware will create a new request to the GET equivalent of the endpoint called and retrieve the current content. After this, a hash of the current content and the If-Match hash will be compared. If the hashes match, the PATCH request will be allowed through the middleware, but if there is no match, 412 will be returned.

Important Since the internal GET request created will also pass through enabled middleware, you might run in to some cases where this is causing issues. For example: if you have a middleware that changes the response body that was not applied to the response that the If-Match etag belongs to, this will result in non-matching hashes.

For this scenario, this middleware sets a X-From-Middleware: IfMatch header which you can use in other middleware to filter these requests. Please note that since this header could also be set by a client, it should never be used to skip anything important like auth middleware.

ifNoneMatch

Method: GET|HEAD

This middleware will simply compare the submitted If-None-Match header to a newly created etag of the response. If there is no match, 200 is returned, with the new response in the case of a GET request. If the hashes are matching, 304 is returned with no content, allowing the browser to used cached content instead.

Comparison algorithms

By default, a weak comparison algorithm will be used for both the IfMatch and IfNoneMatch ETags. In practise this means that we simply strip any W/ tags from the ETag, so they can be compared to normal tags created in the middleware. This is to support cases where certain configurations automatically add the W/ tag to our supplied ETag.

This behaviour can be changed by either publishing the config file:

$ php artisan vendor:publish --provider="Werk365\EtagConditionals\EtagConditionalsServiceProvider"

And then changing the following values:

return [
    'if_match_weak' => env('IF_MATCH_WEAK', true),
    'if_none_match_weak' => env('IF_NONE_MATCH_WEAK', true),
];

Or by setting the ENV values above.

Defining custom ETags

The static method EtagConditionals::etagGenerateUsing() allows you to have full control over how your Etag is generated by passing a callback as argument. This means you can do simple things like returning an ETag using a different algorithm, or other custom solutions.

        EtagConditionals::etagGenerateUsing(function (\Symfony\Component\HttpFoundation\Response $response) {
            return hash('sha256', $response->getContent());
        });
        EtagConditionals::etagGenerateUsing(function (\Illuminate\Http\Request $request, \Symfony\Component\HttpFoundation\Response $response) {
            return Cache::rememberForever('etag.'.$request->url(), function () use ($response) {
                 return md5($response->getContent());
            });
        });

Change log

Please see the changelog for more information on what has changed recently.

Contributing

Feel free to create issues and submit pull requests. For any PR submitted, make sure it is covered by tests or include new tests.

Security

If you discover any security related issues, please email author email instead of using the issue tracker.

Credits

License

Please see the license file for more information.