« Back to Index

Fastly: create, validate, and destroy service

View original Gist on GitHub

Tags: #CLI #Fastly

1. README.md

Compute readthrough cache validator

This directory contains a Compute application that proxies incoming requests onto https://http-me.glitch.me/.

There is a run.sh script which will attempt to validate the responses from the Compute service to see what cache semantics are respected.

[!TIP] Read the official Fastly documentation: readthrough cache

The run.sh script does the following:

[!IMPORTANT] It doesn’t make sense (currently) to try and run this script without real as the input argument, because it causes the script to run the Compute application locally using fastly compute serve, which itself uses https://github.com/fastly/viceroy/ and Viceroy (at the moment) has no support for cache semantics.

POPs and Retries

A request that you might expect to return a cache HIT, could return a MISS. This is because the request can end up at a different POP to where a previous request for the resource ended up.

For example, in the UK there are multiple POPs. Nearest to me are the LHR and LCY POPs. This means I can make a request that ends up at the LHR POP, and if I make a second request and it also ends up at the same POP, then I’ll get a cache HIT, otherwise if the request ends up at the LCY POP I’ll get a cache MISS.

To try and account for this the run.sh script will re-attempt the request a number of times before marking it as unsuccessful. Ultimately, we want to be sure a request is either cached or not cached, so depending on what the expectation is, we give the script the best chance possible to validate the expectation accurately.

Summary of results

Refer to the run.sh script for the details.

Request Method Response Code Response Headers Cacheable
GET 200
GET 200 Cache-Control:max-age=120
GET 200 Surrogate-Control:max-age=120
GET 200 Cache-Control:max-age=120&Surrogate-Control:max-age=120
GET 200 Set-Cookie:foo=bar
GET 200 Cache-Control:no-store
GET 200 Cache-Control:private
GET 200 Surrogate-Control:no-store
GET 200 Surrogate-Control:private
GET 203
GET 300
GET 301
GET 302
GET 400
GET 404
GET 410
GET 500
GET 503
POST 200
POST 200 Cache-Control:max-age=120
POST 200 Surrogate-Control:max-age=120
POST 200 Cache-Control:max-age=120&Surrogate-Control:max-age=120

[!NOTE] The Fastly VCL documentation suggests a 302 is cacheable, but it’s not in Compute.

2. run.sh

#!/usr/bin/env bash

real="$1"

cleanup() {
  if [ "$real" == "real" ]; then
    echo ""
    fastly service delete --force # uses service_id in fastly.toml
  fi
}
trap 'cleanup' ERR

if [ "$real" == "real" ]; then
  fastly compute publish --non-interactive # uses [setup] in fastly.toml to create backend resource
else
  fastly compute serve --verbose & # run in the background
  bg_pid=$! # store the Fastly CLI's Process ID
fi

if [ "$real" == "real" ]; then
  service_id=$(yq eval '.service_id' fastly.toml)
  domain=$(fastly domain list --service-id "$service_id" --version latest --json | jq -r '.[0].Name')
  endpoint="https://$domain"
else
  endpoint="http://127.0.0.1:7676"
fi

if [ "$real" != "real" ]; then
  # wait for the `serve` command to have spun up a local server
  server_port=7676
  max_attempts=10
  attempt=0
  while ! nc -z localhost "$server_port"; do
    if (( attempt == max_attempts )); then
      echo ""
      echo "The local server did not start within the specified number of attempts."
      kill "$bg_pid" # terminate the Fastly CLI running `serve` command in the background
      sleep 2 # give just enough time for Viceroy to setup its listener
      kill "$(lsof -i :7676 | awk 'NR==2 {print $2}')" # terminal Viceroy (CLI might not have a chance to setup signal monitoring to terminate it yet)
      exit 1
    fi
    sleep 1
    (( attempt++ ))
  done
fi

# NOTE: "Fastly-Debug:1" forces the display of the `Surrogate-Control` header.
# We don't set Fastly-Debug because we want to validate Surrogate-Control is omitted from the response.
#
# IMPORTANT: Compute doesn't strip Surrogate-Control for POST requests.
# This is to support VCL service chaining where VCL needs to cache the response.
# Meaning the VCL service requires the Surrogate-Control still.
# The Compute team will investigate if it's possible to fix this so that a
# Compute service will strip the header if not fronted by another Fastly
# service. Now, although POST requests don't strip Surrogate-Control and GET
# requests do, the Viceroy testing tool NEVER strips Surrogate-Control and this
# appears to be related to the fact that it has no cache semantics support.

function check_cacheable() {
  local url=$1
  local needle="x-cache: HIT"
  retries=5
  while [ "$retries" -gt 0 ]; do
    response=$(curl -D - -s "$url")
    if [[ $url == *"Surrogate-Control"* ]]; then
      if [[ $response == *"surrogate-control"* ]]; then
        echo ""
        echo "❌ Surrogate-Control failed to be stripped from the response for $url"
        echo ""
      fi
    fi
    if [[ $url == *"Cache-Control"* ]]; then
      if [[ $response != *"cache-control"* ]]; then
        echo ""
        echo "❌ Cache-Control failed to be found in the response for $url"
        echo ""
      fi
    fi
    if [[ $response == *"$needle"* ]]; then
      echo ""
      echo "✅ Found '$needle' in the response from $url"
      echo ""
      success="true"
      break
    else
      ((retries--))
      success="false"
      sleep 1
    fi
  done
  if [ "$success" != "true" ]; then
    echo ""
    echo "❌ Failed after 5 retries to find '$needle' in the response from $url"
    echo ""
  fi
}

echo ""
echo "Validating cacheable endpoints..."

check_cacheable "$endpoint/anything/status=200"
check_cacheable "$endpoint/anything/status=203"
check_cacheable "$endpoint/anything/status=300"
check_cacheable "$endpoint/anything/status=301"
check_cacheable "$endpoint/anything/status=404"
check_cacheable "$endpoint/anything/status=410"
check_cacheable "$endpoint/anything/status=200?header=Cache-Control:max-age=120"
check_cacheable "$endpoint/anything/status=200?header=Surrogate-Control:max-age=120"
check_cacheable "$endpoint/anything/status=200?header=Surrogate-Control:max-age=240&header=Cache-Control:max-age=120"

function check_uncacheable() {
  local url=$1
  local method=${2:-"GET"}
  local needle="x-cache: HIT"
  retries=5
  surrogate_error_displayed="false"
  cache_error_displayed="false"
  while [ "$retries" -gt 0 ]; do
    response=$(curl -X "$method" -D - -s "$url")
    if [[ $url == *"Surrogate-Control"* && $surrogate_error_displayed == "false" && $method != "POST" ]]; then
      if [[ $response == *"surrogate-control"* ]]; then
        echo ""
        echo "❌ Surrogate-Control failed to be stripped from the response for $method $url"
        echo ""
        surrogate_error_displayed="true"
      fi
    fi
    if [[ $url == *"Cache-Control"* && $cache_error_displayed == "false" ]]; then
      if [[ $response != *"cache-control"* ]]; then
        echo ""
        echo "❌ Cache-Control failed to be found in the response for $method $url"
        echo ""
        cache_error_displayed="true"
      fi
    fi
    if [[ $response == *"$needle"* ]]; then
      echo ""
      echo "❌ Found '$needle' in the response from $method $url"
      echo ""
      success="false"
      break
    else
      ((retries--))
      success="true"
      sleep 1
    fi
  done
  if [ "$success" == "true" ]; then
    echo ""
    echo "✅ After 5 retries '$needle' was NOT found in the response from $method $url"
    echo ""
  fi
}

echo "Validating uncacheable endpoints..."

check_uncacheable "$endpoint/anything/status=200?header=Set-Cookie:foo=bar"
check_uncacheable "$endpoint/anything/status=200?header=Cache-Control:no-store"
check_uncacheable "$endpoint/anything/status=200?header=Cache-Control:private"
check_uncacheable "$endpoint/anything/status=200?header=Surrogate-Control:no-store"
check_uncacheable "$endpoint/anything/status=200?header=Surrogate-Control:private"
check_uncacheable "$endpoint/anything/status=200" "POST"
check_uncacheable "$endpoint/anything/status=200?header=Cache-Control:max-age=120" "POST"
check_uncacheable "$endpoint/anything/status=200?header=Surrogate-Control:max-age=120" "POST"
check_uncacheable "$endpoint/anything/status=200?header=Surrogate-Control:max-age=240&header=Cache-Control:max-age=120" "POST"
check_uncacheable "$endpoint/anything/status=302" # https://www.fastly.com/documentation/reference/vcl/variables/backend-response/beresp-cacheable/ suggested this was cacheable, but it's not
check_uncacheable "$endpoint/anything/status=400"
check_uncacheable "$endpoint/anything/status=500"
check_uncacheable "$endpoint/anything/status=503"

if [ "$real" != "real" ]; then
  kill "$bg_pid" # terminate the Fastly CLI running `serve` command in the background
  kill "$(lsof -i :7676 | awk 'NR==2 {print $2}')" 2>/dev/null # terminal Viceroy if still running (although CLI should have signals setup at this point and would have terminated it already)
fi

cleanup

# NOTE: 3600s (1hr) is XQD's default TTL (VCL services have a 2min TTL).

3. main.go

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/fastly/compute-sdk-go/fsthttp"
)

// BackendName is the origin server incoming requests will be proxied onto.
const BackendName = "httpme"

func main() {
	fsthttp.ServeFunc(func(ctx context.Context, w fsthttp.ResponseWriter, r *fsthttp.Request) {
		start := time.Now()
		resp, err := r.Send(ctx, BackendName)
		if err != nil {
			w.WriteHeader(fsthttp.StatusBadGateway)
			fmt.Fprintln(w, err.Error())
			return
		}
		w.Header().Reset(resp.Header)
		w.Header().Set("X-Execution-Time", time.Since(start).String())
		w.WriteHeader(resp.StatusCode)
		if err := w.Append(resp.Body); err != nil {
			w.WriteHeader(fsthttp.StatusBadGateway)
			fmt.Fprintln(w, err.Error())
			return
		}
	})
}

4. fastly.toml

# This file describes a Fastly Compute package. To learn more visit:
# https://developer.fastly.com/reference/fastly-toml/

authors = ["integralist@fastly.com"]
cloned_from = "https://github.com/fastly/compute-starter-kit-go-default"
description = ""
language = "go"
manifest_version = 3
name = "fastly-readthrough-cache"
service_id = ""

[local_server]

  [local_server.backends]

    [local_server.backends.httpme]
      override_host = "http-me.glitch.me"
      url = "https://http-me.glitch.me/"

[scripts]
  build = "go build -o bin/main.wasm ."
  env_vars = ["GOARCH=wasm", "GOOS=wasip1"]

[setup]

  [setup.backends]

    [setup.backends.httpme]
      address = "http-me.glitch.me"
      description = "HTTP me is a tiny express app initally designed to replicate the features of HTTPBin.org"
      port = 443

5. go.mod

module github.com/domainr/fastly-readthrough-cache

go 1.22

require github.com/fastly/compute-sdk-go v1.3.0