Dancer2::Plugin::ConditionalCaching - RFC7234 Caching


version 0.001


The first three scenarios implicates that no impact in the response is desired. This is accomplished with the parameter check, which should be set to 0.

Using Cache-Control only

Cache-Control is a mechanism to control a cache. In most cases that is not desired, i.e. the user have to make a request to get fresh data. The corrosponding header is called Cache-Control and valid for both request and response. These paramerters control the header in the response:

  • cache

    When 0, set no-cache

  • store

    When 0, set no-store

  • public

    When 1, set public

  • private

    When 1, set private

no-cache and no-store are mutually exclusive. public and private and mutually exclusive, too.

no-cache also eliminates no-store, public and private.

To tell the client, not to cache the response, say:

    caching(check => 0, cache => 0);

To tell the client, not to store the response, say:

    caching(check => 0, store => 0);

To tell the client, sharing the response with everyone is okay, say:

    caching(check => 0, public => 1);

To tell the client, to keep the response private, say:

    caching(check => 0, private => 1);

The keywords must-revalidate and no-transform are sent with every response.

Using time-based headers only

There are three HTTP headers to indicate any time-based state of the response: Age, Expires and Last-Modified.

Last-Modified is redundant to Age, but Last-Modified represents an absolute timestamp whereas Age represents a relative timestamp. Both are controlled by the same parameter.

  • changed

    Accepts a timestamp and calculates Age (now - changed) and set Last-Modified accordingly. Warns if the timestamp is in future - the resulting negative Age value is forced to 0.

  • expires

    Accepts a timestamp and set Expires accordingly. Values in the past are valid.

To tell the client, the response expires in one hour, say:

    caching(check => 0, expires => time + 3600);

To tell the client, the data is created half an hour before, say:

    caching(check => 0, changed => time - 1800);

These headers are only sent when the request method is HEAD or GET.

Using Etag

An Etag is a counter, a checksum, an unique id, adressing a specific state of data. It's up to you what you provide.

The Etag is taken from the parameter etag. The additional boolean parameter weak indicates that the Etag is weak. Weak Etags compares to both weak and strong Etag whereas a strong Etag (i.e. non-weak Etag) compares only to strong Etags. If you don't know what that means, don't think about it. The Etag is strong by default and that's okay.

To tell the client, the response has the Etag abcdef, say:

    caching(check => 0, etag => 'abcdef');

This header is only sent when the request method is HEAD or GET.

Response to a GET or HEAD conditional request

This step is basically accomplished by omitting the check parameter in the examples above. It compares the request headers and then decides to answer the request with 304 Not Modified status code.

A compare against the Etag takes precedence over time-based constraints. Etags are comapred with the If-None-Match request header, time-based constraints are compared with the If-Modified-Since request header.

To check against the Etag abcdef, and exit the current route with 304 if the Etag matches, say:

    caching(etag => 'abcdef');

To check against time-based constraints, say:

    caching(changed => time - 3600);

Response to a POST, PUT, PATCH or DELETE conditional request

Its not really different to the example above. In case of a conflict, the status code is 412 Precondition Failed. Etags are compared aginst If-Match and time-based constraints are compared against If-Unmodified-Since.

No Cache-Control, Expires, Age or Last-Modified headers are sent.

Going deeper: using helper subroutine

It's possible that a client may request a state, which should not expire within a specific amount of time. That is accomplished via the min-fresh field inside the Cache-Control request header. And/or the client may also request a state, which is not older than a specific amount of time. That's accomplished via the max-age field in the Cache-Control request header. Both values are compared with created and expires, and the variable force will be set to accordingly. To access that variable, a builder subroutine can be used, which passes some additionals information:

    caching(builder => sub {
        %opts = @_;
        # requested max-age
        $maxage = $opts{MaxAge};
        # requested min-fresh
        $minfresh = $opts{MinFresh};
        # automatically calculated
        $force = $opts{Force};

Force is 0 by default. When max-age is less than the current age of the data, or when min-fresh is greather than the current freshness (time in seconds till the state expires), then Force is 1.

The result of builder is returned by caching.

    $one_two_three = caching(builder => sub {
        return 123;

If builder is not a CodeRef, the value of that will be returned instead.

    $four_five_six = caching(builder => 456);

Dry and catch

If you don't want any headers to be set, no exception to be thrown and no auto-exit of the current route, then set dry to 1 and check to 0.

    $status_code = caching(dry => 1, check => 0);

The builder subroutine will be still executed, but 200 will be returned instead.

And if you just don't want the auto-exit of the current route, but a HTTP::Exception to be thrown, set throw to 1

    eval {
        caching(throw => 1);
    if (my $e = HTTP::Exception->caught) {
        $status_code = $e->code; # 304 or 412


Please report any bugs or feature requests on the bugtracker website

When submitting a bug or request, please include a test-file or a patch to an existing test-file that illustrates the bug or desired feature.


David Zurborg <>


This software is Copyright (c) 2015 by David Zurborg.

This is free software, licensed under:

  The ISC License