11 things I do in every Fastly service I create
Tags: vcl, edge
Published: 2024-03-26 11:46:30

**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](https://www.fastly.com/documentation/guides/vcl/).

*(If JavaScript, Rust or Go are more your thing, try [Fastly Compute services](https://www.fastly.com/products/compute). 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](https://www.fastly.com/documentation/guides/integrations/backends/), i.e. you have origin servers behind Fastly, it is almost always a good idea to enable [shielding](https://www.fastly.com/documentation/guides/concepts/shielding/), which focuses "miss" traffic into one Fastly [POP](https://www.fastly.com/documentation/guides/concepts/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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tclfvkhw1eq591aigeas.gif)

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](https://www.fastly.com/documentation/reference/api/services/backend/).

Want to know where to shield?  Use the [shield chooser tool](https://www.fastly.com/documentation/guides/concepts/shielding/#choosing-a-shield-location)!

## HTTP/3

Although Fastly negotiates HTTP/2 automatically if your [TLS configuration](https://www.fastly.com/documentation/guides/concepts/routing-traffic-to-fastly/#use-a-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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/esexa25ees4fsy59jduf.gif)

Or drop a single line of VCL into `vcl_recv`:

```vcl
h3.alt_svc();
```

## 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:

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

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:

```vcl
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);
}
```

(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`:

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

## 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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ekodvhjpxnym6xreitw0.gif)

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

```vcl
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 = "";
}
```

## CORS

Trying to build handling for [Cross-origin resource sharing](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 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:

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

Then, in `vcl_error`, construct the preflight response:

```vcl
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);
}
```

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:

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

## Redirects

Handling redirection of out of date URLs is a really big and easy win.  Put your redirects in an [edge dictionary](https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#edge-dictionaries), or if you have pattern-matching redirects, write simple VCL to map the patterns:

```vcl
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;
}
```

Then in `vcl_error`, construct the redirect:

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

## 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:

```vcl
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;
  }
}
```

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](https://www.fastly.com/blog/minimizing-ossification-risk-is-everyones-responsibility) 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](https://www.fastly.com/products/web-application-api-protection), but you can also construct simple rules in VCL using [Access Control Lists](https://www.fastly.com/documentation/guides/concepts/edge-state/dynamic-config/#access-control-lists) or even just basic `if` statements:

```vcl
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;
  }
}
```

Then in `vcl_error`:

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

## 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`:

```vcl
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;
```

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`](https://www.fastly.com/documentation/reference/http/http-headers/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:

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

## 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](https://www.fastly.com/documentation/guides/vcl/best-practices/) 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](https://commuity.fastly.com).