Adds ETag to an Application.

ETags are header information that enable the caching of content. If enabled, RestRserve will return an ETag (eg a hash of a file) alongside the last time it was modified. When a request is sent, additional headers such as If-None-Match, If-Match, If-Modified-Since, and If-Unmodified-Since, can be passed to the server as well.

If the conditions are met (different hash in case of a If-None-Match header or a later file modification in case of a given If-Modified-Since header), the server does not send the requested file but returns a 304 status code, indicating, that the data on the requesting device is up-to-date.

Note that if both headers are provided, the If-None-Match header takes precedence.

Furthermore, the middleware also supports the headers If-Match, which returns the object if the hash matches (it also supports "*" to always return the file), as well as If-Unmodified-Since, which returns the object if it has not been modified since a certain time. If the conditions are not met, a 412 status code is returned (Precondition Failed). See examples below.

References

MDN

Super class

RestRserve::Middleware -> EtagMiddleware

Public fields

hash_function

Function that takes an object or file and computes the hash of it

last_modified_function

Function that takes an object or file and computes the last time it was modified

Methods


Method new()

Creates ETag middleware object

Usage

ETagMiddleware$new(
  routes = "/",
  match = "partial",
  id = "ETagMiddleware",
  hash_function = function(body) {
     if ("file" %in% names(body)) {
        
    digest::digest(file = body[["file"]], algo = "crc32")
     }
     else {
        
    digest::digest(body, algo = "crc32")
     }
 },
  last_modified_function = function(body) {
     if ("file" %in% names(body)) {
       
     as.POSIXlt(file.info(body[["file"]])[["mtime"]], tz = "GMT")
     }
     else {
    
        as.POSIXlt(Sys.time(), tz = "GMT")
     }
 }
)

Arguments

routes

Routes paths to protect.

match

How routes will be matched: exact or partial (as prefix).

id

Middleware id.

hash_function

a function that generates the ETag hash. The function takes the body of the response and returns a single character. Default is crc32 using digest::digest.

last_modified_function

a function that takes the body of the response and returns the last time this was changed. The default is to take the mtime (last time the file was modified) if its a file, if the body does not contain a file, the current time is returned ( resulting in no caching)


Method clone()

The objects of this class are cloneable with this method.

Usage

ETagMiddleware$clone(deep = FALSE)

Arguments

deep

Whether to make a deep clone.

Examples

#############################################################################
# setup a static directory with ETag caching

static_dir = file.path(tempdir(), "static")
if (!dir.exists(static_dir)) dir.create(static_dir)

file_path = file.path(static_dir, "example.txt")
writeLines("Hello World", file_path)

# get the time the file was last modified in UTC time
last_modified = as.POSIXlt(file.info(file_path)[["mtime"]], tz = "UTC")
file_hash = digest::digest(file = file_path, algo = "crc32")

time_fmt = "%a, %d %b %Y %H:%M:%S GMT"



#############################################################################
# setup the Application with the ETag Middleware
app = Application$new()
app$append_middleware(ETagMiddleware$new())
app$add_static(path = "/", static_dir)



#############################################################################
# Example Requests

# Request the file returns the file with ETag headers
req = Request$new(path = "/example.txt")
# note that it also returns the Last-Modified and ETag headers
app$process_request(req)
#> <RestRserve Response>
#>   status code: 200 OK
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13
#>     Last-Modified: Thu, 18 Apr 2024 01:54:58 GMT
#>     ETag: b095e5e3


# provide matching hash of the file in the If-None-Match header to check Etag
# => 304 Not Modified (Can be cached)
req = Request$new(path = "/example.txt",
                  headers = list("If-None-Match" = file_hash))
# note status_code 304 Not Modified
app$process_request(req)
#> <RestRserve Response>
#>   status code: 304 Not Modified
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13


# provide a wrong hash, returns the file normally
req = Request$new(path = "/example.txt",
                  headers = list("If-None-Match" = "WRONG HASH"))
app$process_request(req)
#> <RestRserve Response>
#>   status code: 200 OK
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13
#>     Last-Modified: Thu, 18 Apr 2024 01:54:58 GMT
#>     ETag: b095e5e3


# alternatively, you can provide a timestamp in the If-Modified-Since header
# => 304 Not Modified (Can be cached)
modified_since = format(last_modified + 1, time_fmt)
req = Request$new(path = "/example.txt",
                  headers = list("If-Modified-Since" = modified_since))
app$process_request(req)
#> <RestRserve Response>
#>   status code: 304 Not Modified
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13


# provide both headers: If-None-Match takes precedence
# in this case:
#  - if none match => modified (No cache)
#  - if modified since => NOT MODIFIED (cached)
# => Overall: modified = no cache
modified_since = format(last_modified + 1, time_fmt)
req = Request$new(path = "/example.txt",
                  headers = list("If-None-Match" = "CLEARLY WRONG",
                                 "If-Modified-Since" = modified_since))
app$process_request(req)
#> <RestRserve Response>
#>   status code: 200 OK
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13
#>     Last-Modified: Thu, 18 Apr 2024 01:54:58 GMT
#>     ETag: b095e5e3


# provide matching hash of the file in the If-Match header to check Etag
# => 412 Precondition Failed
req = Request$new(path = "/example.txt",
                  headers = list("If-Match" = "OTHER HASH"))
# note status_code 412 Precondition Failed
app$process_request(req)
#> <RestRserve Response>
#>   status code: 412 Precondition Failed
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13


# Use If-Unmodified-Since
unmodified_since = format(last_modified - 1, time_fmt)
req = Request$new(path = "/example.txt",
                  headers = list("If-Unmodified-Since" = unmodified_since)
)
# note status_code 412 Precondition Failed
app$process_request(req)
#> <RestRserve Response>
#>   status code: 412 Precondition Failed
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13



#############################################################################

# use an alternative hash function (use name of the file)
hash_on_filename = function(x) x
# also use an alternate last_modified time function
always_1900 = function(x) as.POSIXlt("1900-01-01 12:34:56", tz = "GMT")


# setup the app again
app = Application$new(middleware = list(
  ETagMiddleware$new(hash_function = hash_on_filename,
                     last_modified_function = always_1900)
))
app$add_static(path = "/", file_path = static_dir)


# test the requests
req = Request$new(path = "/example.txt")
(res = app$process_request(req))
#> <RestRserve Response>
#>   status code: 200 OK
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13
#>     Last-Modified: Mon, 01 Jan 1900 12:34:56 GMT
#>     ETag: /private/var/folders/24/8k48jl6d249_n_qfxwsl6xvm0000gn/T/RtmpTW5Z2o/static/example.txt

filename = res$body[["file"]]
req = Request$new(path = "/example.txt",
                  headers = list("If-None-Match" = filename))
app$process_request(req)
#> <RestRserve Response>
#>   status code: 304 Not Modified
#>   content-type: text/plain
#>   <Headers>
#>     Server: RestRserve/1.2.2; Rserve/1.8.13