Web applications may send a special HTTP method OPTIONS to query an API for functionality. If supported, the answer is a bunch of clear text HTTP headers. An OPTIONS request is usually quite lightweight for the server, but it still uses resources like connections and CPU time. The answer to an OPTIONS request seldom changes unless the API itself changes. So we have small HTTP text objects that seldom are updated. Sounds ideal for caching with Varnish.

Varnish is the ultimate HTTP cache. If you don’t know what Varnish is, think of it as an ultra fast caching web proxy. Powered by the dark side of the force. On steroids. It also has a rich configuration language, and programmable interface, making it ideal as a level 7 Swiss army knife. For 12 use cases of Varnish, see my Sysadvent post 12 days of varnish.

A customer asked us to start caching OPTIONS requests, observing a large amount of OPTIONS call to their API servers, as much of the normal GET requests were already cached in Varnish.

Varnish does not cache OPTIONS by default. From vcl_recv in the builtin VCL:

    if (req.method != "GET" && req.method != "HEAD") {
        /* We only deal with GET and HEAD by default */
        return (pass);
    }
    if (req.http.Authorization || req.http.Cookie) {
        /* Not cacheable by default */
        return (pass);
    }
    return (hash);

Tip: Use varnishd -x builtin to read the builtin VCL.

The functions in the builtin VCL are appended to the corresponding functions in our custom VCL (i.e. /etc/varnish/default.vcl), unless we terminate the function with an explicit return. Which is what we need here: As we see above, all methods except GET and HEAD get a return (pass). But we want to return (hash) also for OPTIONS. As the builtin VCL can not be changed, we have to add an explicit return (hash) ourselves. Note that the builtin VCL is there for a reason, so if we short-circuit it with a return, we should replicate the interesting parts of the builtin VCL as well. We put this in a separate function to avoid cluttering vcl_recv too much.

sub method_options {

   /* The built-in vcl_recv overrides pass on OPTIONS, but we want to
    * run some parts of it. So we add stuff from built-in vcl_recv here
    */

   if (!req.http.host &&
     req.esi_level == 0 &&
     req.proto ~ "^(?i)HTTP/1.1") {
       /* In HTTP/1.1, Host is required. */
       return (synth(400));
   }

   if (req.http.Authorization || req.http.Cookie) {
        /* Not cacheable by default */
        return (pass);
    }

    /* Done with stuff from built-in vcl */


    /*  Save the original method (ie. OPTIONS) for later */
    set req.http.X-OrigMethod = req.method;

    /* Explicit return hash */
    return (hash);
}

Note that we save the request’s method in a header X-OrigMethod. More on this later

Now we may call this function from vcl_recv on an OPTIONS match:

sub vcl_recv {

    // (...)

    /* This should be added at the very end of vcl_recv, as it overrides
     * the built-in vcl for OPTIONS
     */
    if (req.method == "OPTIONS") { call method_options; }
}

Note that we do this as close to the very end of vcl_recv as possible to make sure that the builtin VCL runs for all other requests.

Next, to avoid that cached GET and OPTIONS objects with the same URL get mixed, we add the method to the hash, so they are stored separately:

sub vcl_hash {
    /* Hash on method to avoid getting OPTION output in GET requests
     * and vica verca
     */
    if (req.method == "OPTIONS" ) {
        hash_data(req.method);
    }
}

(We could drop the if test there, and add the method to the hash for all requests if we like.)

This should be enough to get caching of OPTIONS working. But it is not. In their eternal wisdom, the Varnish developers convert an OPTIONS request to a GET when the backend client starts working. So we have to set it explicitly back in vcl_backend_fetch. Remember we saved the method in a header? It now becomes handy:

sub vcl_backend_fetch {

    /* Here be dragons: Varnish will automagically convert the OPTIONS
     * to GET after hashing, so set it back */

    if (bereq.http.X-OrigMethod == "OPTIONS") {
      set bereq.method = bereq.http.X-OrigMethod;
    }
}

With this last piece in place, varnish should cache OPTIONS fine. Custom TTL may be set in vcl_backend_response, for example like this:

sub vcl_backend_response {
    if (bereq.http.x-method == "OPTIONS") {
        set beresp.ttl = 1800s;
    }
}

Use curl for easy testing:

curl -s -X OPTIONS -D - 'https://api.example.com/path/to/endpoint/v42/'

-D - includes headers in the output to stdout. Look for the Age: header. It should increase on repeated requests.

There is a final problem that we have not looked at: Purging content. A PURGE request does not include the original method for an object stored in the cache. To add support for purging both GET and OPTIONS object from the cache, use a magic header, something like this:

acl purge {
    "localhost";
}

sub vcl_recv {
    if (req.method == "PURGE") {

        if (!client.ip ~ purge) {
            return(synth(405,"Not allowed."));
        }

        /* Use a magic header X-Method for purging OPTIONS requests
         * Like curl -H "X-Method: OPTIONS" -X PURGE ...
         */
        if ( req.http.X-Method == "OPTIONS" ) {
            set req.method = req.http.X-Method;
        }

        return (purge);
    }

To request PURGE to an OPTIONS object in the cache, you may now do

curl -s -X PURGE -H "X-Method: OPTIONS" 'https://api.example.com/path/to/endpoint/v42/'

Conclusion: HTTP OPTIONS requests are well suited for caching. Varnish will cache OPTIONS request well with a little configuration.



Redpill Linpro is the Open Source leader in the Nordics, helping customers with the digital transformation since back in the nineties.

Great thanks to Varnish Software for maintaining the Open Source 6.0 LTS branch of Varnish Cache. Also shout-out to Simon of one.com on IRC for patiently explaining me some of the difficult parts above.


Update

  • 2024-02-19 Change credit link in header.

Ingvar Hagelund

Team Lead, Application Management for Media at Redpill Linpro

Ingvar has been a system administrator at Redpill Linpro for more than 20 years. He is also a long time contributor to the Fedora and EPEL projects.

Just-Make-toolbox

make is a utility for automating builds. You specify the source and the build file and make will determine which file(s) have to be re-built. Using this functionality in make as an all-round tool for command running as well, is considered common practice. Yes, you could write Shell scripts for this instead and they would be probably equally good. But using make has its own charm (and gets you karma points).

Even this ... [continue reading]

Containerized Development Environment

Published on February 28, 2024

Ansible-runner

Published on February 27, 2024