11 things I do in every Fastly service I create

Andrew Betts - Mar 26 - - Dev Community

The variety of things you can do in edge computing these days is huge, and Fastly customers are constantly impressing me with their creativity and the complexity of some of the problems that can be solved entirely at the edge. But there are some things that almost regardless of your use case, you probably should be doing.

I've set up a lot of Fastly services, and along the way I've realized that pretty much every time, I add the same patterns to do the same useful stuff. Whether it's an e-commerce site, something statically generated, a SaaS service, or anything else, there's a set of stuff that is almost universally applicable. Let's check them off here, with examples for Fastly's Delivery (VCL) services.

(If JavaScript, Rust or Go are more your thing, try Fastly Compute services. It's free to sign up and you can do a lot of the same things I've listed here)

Shielding

If your Fastly service uses backends, i.e. you have origin servers behind Fastly, it is almost always a good idea to enable shielding, which focuses "miss" traffic into one Fastly POP before sending it to your origin. You then have two chances to get a hit in the cache, and the cache in the shield POP will typically become larger and more comprehensive than other POPs, so this can reduce traffic to your origin servers by a huge amount: up to two orders of magnitude!

Shielding UI

Shielding needs to be enabled as a property of the backend configuration, either in the control panel or when creating a backend via the Fastly API.

Want to know where to shield? Use the shield chooser tool!

HTTP/3

Although Fastly negotiates HTTP/2 automatically if your TLS configuration supports it, at time of writing we require you to explicitly enable H3 by adding an Alt-Svc header to your responses - but the good news is that you can enable this in the control panel:

H3 UI

Or drop a single line of VCL into vcl_recv:



h3.alt_svc();


Enter fullscreen mode Exit fullscreen mode

Force TLS

Another quick win is forcing all clients to use TLS. We do accept connections on plain HTTP in VCL services, so it's a good idea to redirect them to HTTPS. This can be done using a toggle in the control panel, right next to the HTTP/33 option.

Or use custom VCL:



if (req.protocol != "https") {
  error 608;
}


Enter fullscreen mode Exit fullscreen mode

We recommend throwing error codes in the 6xx range and then mapping them to conventional HTTP status codes in vcl_error, where you can also turn the parameter into a Location header:



if (obj.status == 608) {
  set obj.http.location = "https://" req.http.host req.url;
  set obj.status = 308;
  set obj.response = "Moved permanently";
  return (deliver);
}


Enter fullscreen mode Exit fullscreen mode

(by the way, if you're using a Compute service, this redirect happens automatically, and no insecure traffic ever reaches your service)

HTTP Strict Transport Security

You should also return a strict-transport-security header to ensure that clients never make an insecure connection. If you use the control panel to configure Force TLS, HSTS will be set up as well, but if you're using custom VCL, adding response headers just before sending to the client can be done in vcl_deliver:



set resp.http.strict-transport-security = "max-age=31536000; includeSubDomains; preload"


Enter fullscreen mode Exit fullscreen mode

Compression

The final option that is configurable in the control panel, enable compression under Content > Compression, and we will automatically compress any uncompressed responses from origin.

Compression UI

If you want to do this in VCL, take care to update the Vary header to include "Accept-Encoding":



if (beresp.status == 200 && beresp.http.content-type ~ "^(?:text/|application/json)") {
  if (req.http.Accept-Encoding == "br") {
    set beresp.brotli = true;
  } elsif (req.http.Accept-Encoding == "gzip") {
    set beresp.gzip = true;
  }
  set beresp.http.Vary:Accept-Encoding = "";
}


Enter fullscreen mode Exit fullscreen mode

CORS

Trying to build handling for Cross-origin resource sharing into your backend app is often a pain. Instead, handle CORS preflights at the edge and ensure clients get the right CORS rules. In vcl_recv you can trap OPTIONS requests:



if (req.method == "OPTIONS" && req.http.Origin) {
  error 612;
}


Enter fullscreen mode Exit fullscreen mode

Then, in vcl_error, construct the preflight response:



if (obj.status == 612) {
  set obj.status = 204;
  set obj.http.access-control-allow-origin = req.http.origin;
  set obj.http.access-control-max-age = "86400";
  return(deliver);
}


Enter fullscreen mode Exit fullscreen mode

Finally, in vcl_deliver, you can add the headers specifying what is allowed, since you need to add these to all responses, not just the preflight ones:



set resp.http.access-control-allow-methods = "GET,HEAD,POST,OPTIONS";
set resp.http.access-control-allow-headers = "Content-Type, x-requested-with";


Enter fullscreen mode Exit fullscreen mode

Redirects

Handling redirection of out of date URLs is a really big and easy win. Put your redirects in an edge dictionary, or if you have pattern-matching redirects, write simple VCL to map the patterns:



declare local var.dest STRING;

// Lookup in table / edge dictionary
if (table.contains(my_redirects, req.url.path)) {  
  set var.dest = table.lookup(my_redirects, req.url.path);

// Special case pattern
} else if (req.url.path ~ "^/products/(\w+)/([0-9]+)") {
  set var.dest = "/catalog/categories/" re.group.1 "/products/" re.group.2;
}
if (var.dest) {
 error 601 var.dest;
}


Enter fullscreen mode Exit fullscreen mode

Then in vcl_error, construct the redirect:



if (obj.status == 601) {
  set obj.http.location = obj.response;
  set obj.status = 308;
  set obj.response = "Permanent redirect";
  return (deliver);
}


Enter fullscreen mode Exit fullscreen mode

Normalizing requests for better cache performance

If you have a clear idea of what constitutes a valid URL for your site, it's helpful to use VCL to filter out anything else:



if (fastly.ff.visits_this_service == 0 && req.restarts == 0) {

  # Sort query params into alphabetical order
  set req.url = querystring.sort(req.url);

  # Media assets have a specific set of allowed query params
  if (req.url.path ~ "/media_[0-9a-f]{40,}[/a-zA-Z0-9_-]*\.[0-9a-z]+\z") {
    # width, height, format, optimize
    set req.url = querystring.filter_except(req.url,
      "width" querystring.filtersep()
      "height" querystring.filtersep()
      "format" querystring.filtersep()
      "optimize"
    );

  # Paths ending .json have a different, specific set of allowed params
  } else if (req.url.ext == "json") {
    # limit, offset, sheet
    set req.url = querystring.filter_except(req.url,
      "limit" querystring.filtersep()
      "offset" querystring.filtersep()
      "sheet"
    );

  # Otherwise, query params are not allowed at all
  } else {
    set req.url = req.url.path;
  }
}


Enter fullscreen mode Exit fullscreen mode

Not all websites have the luxury of knowing what parameters might be on the query strings of requests - and sometimes even if you do, you don't want to turn today's reality into tomorrow's limitation. After all ossification of the web is a major problem.

Blocking unwanted traffic

If you want a comprehensive solution to block malicious traffic, check out our Next-Gen WAF at Edge, but you can also construct simple rules in VCL using Access Control Lists or even just basic if statements:



if (fastly.ff.visits_this_service == 0 && req.restarts == 0) {
  if (
    req.http.user-agent ~ "/bot/" || // Header pattern
    !req.http.accept ||              // Required header
    req.http.via ||                  // Banned header
    client.geo.country_code ~ "^(fr|uk|ru)$" || // Country of origin
    client.ip ~ my_acl ||            // IP is in an ACL
    client.as.number == 12345 ||     // Client's ISP
  ) {
    error 625;
  }
}


Enter fullscreen mode Exit fullscreen mode

Then in vcl_error:



if (obj.status == 625) {
  set obj.status = 403;
  set obj.response = "Forbidden";
  synthetic "Not allowed";
  return(deliver);
}


Enter fullscreen mode Exit fullscreen mode

Strip upstream headers

If you're using AWS S3, Google Cloud Storage or similar as a backend, they will likely return a whole bunch of response headers that you don't want. Remove them with some VCL in vcl_fetch:



unset beresp.http.server;
unset beresp.http.x-generator;
unset beresp.http.via;
unset beresp.http.x-github-request-id;
unset beresp.http.x-amz-delete-marker;
unset beresp.http.x-amz-id-2;
unset beresp.http.x-amz-request-id;
unset beresp.http.x-amz-version-id;
unset beresp.http.x-goog-component-count;
unset beresp.http.x-goog-expiration;
unset beresp.http.x-goog-generation;
unset beresp.http.x-goog-metageneration;
unset beresp.http.x-goog-stored-content-encoding;
unset beresp.http.x-goog-stored-content-length;


Enter fullscreen mode Exit fullscreen mode

Doing this in vcl_fetch makes sense because it happens before the object is written to cache.

Debugging

By default, Fastly includes certain response headers when a Fastly-Debug header is present in the request. It's handy to use this mechanism to add any other debugging information you want to emit. For example, you could include the final request path that was used to look up the object in cache, and the version of your Fastly service that handled the request:



if (req.http.fastly-debug) {
set resp.http.fastly-service-version = req.vcl.version;
set resp.http.fastly-req-path = req.url.path;
}

Enter fullscreen mode Exit fullscreen mode




You're off to a great start!

These patterns are a great way to learn about the power of the edge to simplify and decouple layers of your service architecture, and apply to almost any use case. Implementing these also helps you understand some common best practices of VCL and set you up to implement solutions more specific to your use cases.

Whatever you create, share it with us below or at community.fastly.com.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .