Cache Expire

Problem this snippet solves:

This iRule sets caching headers Expires and Cache-Control on the response. Mostly the use case will be to set client-side caching of static resources as images, stylesheets, javascripts etc.. It will honor, by default, the headers set by the origin server, then give precedence to the resource's mime-type (over it's file-extension). Also, if the origin server supplies the mime-type of the resource then file-extension entries are not considered.

Code :

# Expires iRule, Version 0.9.1
# August, 2012

# Created by Opher Shachar (contact me through devcentral.f5.com)
# (please see end of iRule for additional credits)

# Purpose:
# This iRule sets caching headers on the response. Mostly the use case will be
# to set client-side caching of static resources as images, stylesheets, 
# javascripts etc.

# Configuration Requirements:
# Uses a datagroup named Expires of type string containing lines that
# specify mime-types or extention for the name and, the number of seconds the
# client should cache the resource for the value. ex.:
#    "image/" := "604800"
#    ".js"    := "604800"
# The Content-Type, if specified, takes precedence over the file extention.

when RULE_INIT {
   # Enable to debug Expires via log messages in /var/log/ltm
   # (2 = verbose, 1 = essential, 0 = none)
   set static::ExpiresDebug 0

   # Overwrite cache headers in response
   # (1 = yes, 0 = no)
   set static::ExpiresOverwrite 0
}

when CLIENT_ACCEPTED {
   # The name of the Data Group (aka class) we are going to use
   set vname [URI::basename [virtual name]]
   set vpath [URI::path [virtual name]]
   set Expires_clname "${vpath}Expires$vname"

   if {! [class exists $Expires_clname]} {
      log local0.notice "Data Group $Expires_clname not found."
   }
}

when HTTP_REQUEST {
   # The log prefix so you can find yourself in the log
   set Expires_lp "VS=[virtual name], URI=[HTTP::uri]"

   if {[class exists $Expires_clname]} {
      set period [string last . [HTTP::path]]
      if { $period >= 0 } {
         # Set the timeout based on the class entry if it exists for this request.
         set expire_content_timeout [class match -value [string tolower [getfield [string range [HTTP::path] $period end] ";" 1]] ends_with $Expires_clname]
         if { ($static::ExpiresDebug > 1) and ($expire_content_timeout ne "") } {
           log local0. "$Expires_lp: found file suffix based expiration: $expire_content_timeout."
         }
      }
      else {
         set expire_content_timeout ""
      }
   }
}

when HTTP_RESPONSE {
   if { [info exists expire_content_timeout] } { # if expire_content_timeout not set then no class was found
      if { [HTTP::header exists "Content-Type"] } {
         # Set the tiemout based on the class entry if it exists for this mime type.
         # TODO: Allow globbing in matching mime-types
         set expire_content_timeout [class match -value [string tolower [HTTP::header "Content-Type"]] starts_with $Expires_clname]
         if { ($static::ExpiresDebug > 1) and ($expire_content_timeout ne "") } {
            log local0. "$Expires_lp: found mime type based expiration: $expire_content_timeout."
         }
      }
      if { $expire_content_timeout ne "" } { # either matched Content-Type or file extention
         if { $static::ExpiresOverwrite or not [HTTP::header exists "Expires"] } {
            HTTP::header replace "Expires" "[clock format [expr ([clock seconds]+$expire_content_timeout)] -format "%a, %d %h %Y %T GMT" -gmt true]"
            if { ($static::ExpiresDebug > 0) } {
               log local0. "$Expires_lp: Set 'Expires' to '[clock format [expr ([clock seconds]+$expire_content_timeout)] -format "%a, %d %h %Y %T GMT" -gmt true]'."
            }
         }
         elseif { [HTTP::header exists "Expires"] } {
            set expire_content_timeout [expr [clock scan "[HTTP::header Expires]" -gmt true] - [clock seconds]]
            if { $expire_content_timeout < 0 } {
               if { ($static::ExpiresDebug > 0) } {
                  log local0. "$Expires_lp: Found 'Expires' header either invalid or in the past."
               }
               return
            }
            if { ($static::ExpiresDebug > 0) } {
               log local0. "$Expires_lp: Found 'Expires' header and calculated $expire_content_timeout seconds timeout."
            }
         }
         if { $static::ExpiresOverwrite or not [HTTP::header exists "Cache-Control"] } {
            HTTP::header replace "Cache-Control" "max-age=$expire_content_timeout, public"
            if { ($static::ExpiresDebug > 0) } {
               log local0. "$Expires_lp: Set 'Cache-Control' to 'max-age=$expire_content_timeout, public'."
            }
         }
      }
   }
}
Published Jan 30, 2015
Version 1.0
  • Thanks for the code.

     

    I agree with Vova regarding the second problem and below is the updated iRule to solve it. Regarding the first problem, I don't agree and I think that this is a good design choice to consider only the last extension when there is more than one. I have many .min.js and .min.css and they are all matching my .js and .css data group entries.

     

         Expires iRule, Version 0.9.1
     August, 2012
    
     Created by Opher Shachar (contact me through devcentral.f5.com)
     (please see end of iRule for additional credits)
    
     Purpose:
     This iRule sets caching headers on the response. Mostly the use case will be
     to set client-side caching of static resources as images, stylesheets, 
     javascripts etc.
    
     Configuration Requirements:
     Uses a datagroup named Expires of type string containing lines that
     specify mime-types or extention for the name and, the number of seconds the
     client should cache the resource for the value. ex.:
        "image/" := "604800"
        ".js"    := "604800"
     The Content-Type, if specified, takes precedence over the file extention.
    
    when RULE_INIT {
        Enable to debug Expires via log messages in /var/log/ltm
        (2 = verbose, 1 = essential, 0 = none)
       set static::ExpiresDebug 0
    
        Overwrite cache headers in response
        (1 = yes, 0 = no)
       set static::ExpiresOverwrite 0
    }
    
    when CLIENT_ACCEPTED {
        The name of the Data Group (aka class) we are going to use
       set vname [URI::basename [virtual name]]
       set vpath [URI::path [virtual name]]
       set Expires_clname "${vpath}Expires$vname"
    
       if {! [class exists $Expires_clname]} {
          log local0.notice "Data Group $Expires_clname not found."
       }
    }
    
    when HTTP_REQUEST {
        The log prefix so you can find yourself in the log
       set Expires_lp "VS=[virtual name], URI=[HTTP::uri]"
    
       if {[class exists $Expires_clname]} {
          set period [string last . [HTTP::path]]
          if { $period >= 0 } {
              Set the timeout based on the class entry if it exists for this request.
             set expire_content_timeout [class match -value [string tolower [getfield [string range [HTTP::path] $period end] ";" 1]] ends_with $Expires_clname]
             if { ($static::ExpiresDebug > 1) and ($expire_content_timeout ne "") } {
               log local0. "$Expires_lp: found file suffix based expiration: $expire_content_timeout."
             }
          }
          else {
             set expire_content_timeout ""
          }
       }
    }
    
    when HTTP_RESPONSE {
       if { [info exists expire_content_timeout] } {  if expire_content_timeout not set then no class was found   
          if { [HTTP::header exists "Content-Type"] } {
            if {[class match [string tolower [HTTP::header "Content-Type"]] starts_with $Expires_clname]} {
                  Set the tiemout based on the class entry if it exists for this mime type.
                  TODO: Allow globbing in matching mime-types
                 set expire_content_timeout [class match -value [string tolower [HTTP::header "Content-Type"]] starts_with $Expires_clname]
                 if { ($static::ExpiresDebug > 1) and ($expire_content_timeout ne "") } {
                    log local0. "$Expires_lp: found mime type based expiration: $expire_content_timeout."
                }
            }
          }
    
          if { $expire_content_timeout ne "" } {  either matched Content-Type or file extention  
             if { $static::ExpiresOverwrite or not [HTTP::header exists "Expires"] } {
                HTTP::header replace "Expires" "[clock format [expr ([clock seconds]+$expire_content_timeout)] -format "%a, %d %h %Y %T GMT" -gmt true]"
                if { ($static::ExpiresDebug > 0) } {
                   log local0. "$Expires_lp: Set 'Expires' to '[clock format [expr ([clock seconds]+$expire_content_timeout)] -format "%a, %d %h %Y %T GMT" -gmt true]'."
                }
             }
             elseif { [HTTP::header exists "Expires"] } {
                set expire_content_timeout [expr [clock scan "[HTTP::header Expires]" -gmt true] - [clock seconds]]
                if { $expire_content_timeout < 0 } {
                   if { ($static::ExpiresDebug > 0) } {
                      log local0. "$Expires_lp: Found 'Expires' header either invalid or in the past."
                   }
                   return
                }
                if { ($static::ExpiresDebug > 0) } {
                   log local0. "$Expires_lp: Found 'Expires' header and calculated $expire_content_timeout seconds timeout."
                }
             }
             if { $static::ExpiresOverwrite or not [HTTP::header exists "Cache-Control"] } {
                HTTP::header replace "Cache-Control" "max-age=$expire_content_timeout, public"
                if { ($static::ExpiresDebug > 0) } {
                   log local0. "$Expires_lp: Set 'Cache-Control' to 'max-age=$expire_content_timeout, public'."
                }
             }
          }
       }
    }
    
  • Hi, Good job. For my use case I found a few problems with this rule. 1. When you have double extensions (like file.xml.md5) it's not working. The "set period" section needs to be removed. 2. In HTTP_RESPONSE is good to add a check if we have mime type in the datagroup otherwise it will overwrite expire_content_timeout with "". Let me know if you want to see my tuned rule