Proper error pages in Varnish Cache

If you have ever seen the default Varnish guru error page, you know what I'm talking about.

If your backend is down (503), Varnish will show you a pretty basic (yet ugly) page about a server error:

And there are several other errors you can "use" in Varnish:

  • Forced not found page -> 404 error page
  • Access denied -> 401 or 403
  • Wrong http method -> 405

Examples of responding with an error yourself

sub vcl_recv {
  if (req.method == "PURGE") {
    if (client.ip !~ purge) {
      return (synth(405, "Method not allowed")); // [tl! focus]
    }
	
	return (purge);
  }

  if (req.url ~ "(?i)(old-folder|even-older-folder|page-url-does-not-exist)") {
    return (synth(404, "Not found")); // [tl! focus]
  }

  if (req.http.User-Agent ! "(?i)(bad bot|ugly bot|not-so-nice spider|aggressive crawler)") {
    return (synth(403)); // [tl! focus]
  }
}

How to process these errors in Varnish

synth(status, reason) calls are processed in the vcl_synth subroutine which you can modify. There are two ways you can modify the response content:

  1. Use some inline content
  2. Use a file with content

Custom inline content

sub vcl_synth {
  if (resp.status == 404) {
    set resp.http.Content-Type = "text/html; charset=utf-8";
    set resp.http.Cache-Control = "public, max-age=86400";

    set resp.body = {"<!DOCTYPE html>
      <html>
        <head>
          <meta charset="utf-8">
          <title>404 - Not found</title>
        </head>
        <body>
          <h1>404 - Not found</h1>
        </body>
      </html>
    "};

    return (deliver);
  }
}

Here you set some headers yourself to tell the Browser what content-type to expect, and how to handle it cache-wise, in this case: cache it for one day.

If you make changes in your config with an inline HTML synth response, and reload your Varnish configuration without restarting Varnish, your inline HTML changes take effect immediately, since such responses are generated on the fly.

Custom inline content with dynamic elements

You can also add some existing variables from the request or response object available at that stage. A perfect example is the default Varnish error message:

set resp.body = {"<!DOCTYPE html>
  <html>
    <head>
      <title>"} + resp.status + " " + resp.reason + {"</title>
    </head>
    <body>
      <h1>Error "} + resp.status + " " + resp.reason + {"</h1>
      <p>"} + resp.reason + {"</p>
      <h3>Guru Meditation:</h3>
      <p>XID: "} + req.xid + {"</p>
      <hr>
      <p>Varnish cache server</p>
    </body>
  </html>
"};

This can be helpful if you are behind a load balancer and need to show the currently used server, or some other information which could be helpful.

Custom content from a file

sub vcl_synth {
  if (resp.status == 503) {
    set resp.http.Content-Type = "text/html; charset=utf-8";
    set resp.http.Cache-Control = "no-store";
	set resp.http.Retry-After = "5";
    synthetic(std.fileread("/var/www/html/503.html"));

    return(deliver);
  }
}

For this specific error a file will be read once from /var/www/html/503.html. The browser will be instructed never to cache it at all, in case of this error going away sometime later. And the browser is instructed to retry again after 5 seconds.

Be aware: If you make any changes in your referenced error file and reload your Varnish config without restarting Varnish, your new changes won't be considered since the file content will be read once and cached. You have to restart Varnish.

Not only HTML

You don't need to use HTML at all. If you are dealing with JSON or XML requests, you can respond with JSON or XML. Also either with inline content or a content file.

Include all necessary content

If you need some extra design for your error page, don't rely on being able to load additional files, like css, js or images.

Maybe your backend is down, so nothing would go anyway. Or you block a certain user agent and it's a false positive. Or you have some rate limiter in place and a human visitor runs into it.

As much as possible: include all necessary content inline so it's available with a single response of your error page.

Important headers

Ensure that you set some proper cache-control header like "no-store" if you are certain that this output should not be cached by the browser, and each subsequent request should be processed again, or a cache-control header like "public, max-age=86400" to tell the browser (and other proxies in between) to cache that url for one day.

Header cleanup

Usually Varnish responses will be finished with a call to vcl_deliver where you can do some final header cleanup, like removing debug headers. This subroutine won't be called for synth responses. So if you want to do some header processing you should do that in vcl_synth too. My advice is to create a subroutine for header cleanup and call that on both vcl_deliver and vcl_synth, like this:

sub custom_final_header_cleanup {
  unset resp.http.X-Powered-By;
  unset resp.http.Server;
  unset resp.http.X-URL;
  unset resp.http.X-Host;
  ...
}

sub vcl_deliver {
	call custom_final_header_cleanup;
}

sub vcl_synth {
	call custom_final_header_cleanup;
}