Georges Menie

Manage If-Modified-Since request with ESI

Blog Post created by Georges Menie Employee on Jun 1, 2017

ESI (Edge Side Include) is a way to build a response based on code that is executed directly at the edge. When you use ESI, the Last-Modified header is not sent to the browser, like when you are using any server-side language to generate a page (php, asp, jsp, etc.).

 

If your ESI result is cacheable and you have a timestamp that could be used to validate the cached version of the result in the browser, you could use the Last-Modified header and let the browser perform the corresponding If-Modified-Since request to revalidate its cache. This is the goal of this article, we are not discussing here  the request flow between the edge server and the origin (If-Modified-Since request are used there), only the request flow between the browser and the edge.

 

Simple case: one-file ESI code

 

Let's start with a one-file ESI page, you could use the following ESI code to generate the necessary headers and handle the If-Modified-Since requests:

 

<esi:assign name="lmt" value="'Tue, 21 Feb 2017 16:12:00 GMT'"/>
<esi:choose>
    <esi:when test="$(HTTP_IF_MODIFIED_SINCE) == $(lmt)">
        $set_response_code(304)
    </esi:when>
    <esi:otherwise>
        $add_header('Last-Modified', $(lmt))
        <!--
             Add your initial page content / esi code here
        -->
    </esi:otherwise>
</esi:choose>

 

The fisrt line contains the timestamp to be used, it has to be generated from the origin server and its type has to be a string, this is the reason to generate value="'...'" the single-quote inside the double-quote makes a variable of type string.

 

The date format to be used is normalized, with php you could use :

 

date_default_timezone_set('UTC');

...

$lmt = $ts->format('D, d M Y H:i:s').' GMT';

 

Complex rule with one ESI template and several fragments

 

If your page is being build from several fragments that are all cacheable, you may decide to use the more recent timestamp from the template and any fragments as the validation timestamp.

 

In this example, the template is build from 3 fragments, the code to add is at the end of the template like this :

 

<html>
<body>
<esi:eval src="frag1_esi.html" dca="none"/>
<esi:eval src="frag2_esi.html" dca="none"/>
<esi:eval src="frag3_esi.html" dca="none"/>
</body>
</html>

 

<esi:assign name="gentime" value="1495548176"/>
<esi:choose>
    <esi:when test="!$exists($(reftime)) || $(reftime) < $(gentime)">
        <esi:assign name="reftime" value="$(gentime)"/>
    </esi:when>
</esi:choose>
<esi:assign name="lmt" value="$strftime($(reftime), '%a, %d %b %Y %H:%M:%S %Z')"/>
<esi:choose>
    <esi:when test="$(HTTP_IF_MODIFIED_SINCE) == $(lmt)">
        $set_response_code(304,'')
    </esi:when>
    <esi:otherwise>
        $add_header('Last-Modified', $(lmt))
    </esi:otherwise>
</esi:choose>

 

Note that now we are using an integer as timestamp (gentime variable) this is necessary so that we can compare the timestamp from the template and its fragments. Note also that every fragments is fetched with dca="none" so that the esi code in the fragments gets executed in the template context, this is mandatory.

 

Now, each fragment should contain the following code :

 

<p> your fragment content here

 

<esi:assign name="gentime" value="1495548006"/>
<esi:choose>
    <esi:when test="!$exists($(reftime)) || $(reftime) < $(gentime)">
        <esi:assign name="reftime" value="$(gentime)"/>
    </esi:when>
</esi:choose>

 

Each fragment may have its own timestamp stored as integer into the gentime variable. When the ESI processor evaluate the fragments, the code compare the current reference time with the fragment gentime, keeping the latest value to be use eventually as the Last-Modified time in the template.

 

Please note that the template and all the fragments are still evaluated at each request, this setting does not save server-side cpu but only the transfer time and bandwith when 304 can be returned.

Outcomes