Extended VCL compiler with metaprogramming features for Fastly VCL
Project description
xvcl
Supercharge your Fastly VCL with programming constructs like loops, functions, constants, and more.
Quick Reference Guide - One-page syntax reference for all xvcl features
Table of Contents
- Introduction
- Why Use xvcl?
- Installation
- Quick Start
- Core Features
- Advanced Features
- Command-Line Usage
- Integration with Falco
- Best Practices
- Troubleshooting
Introduction
xvcl is a VCL transpiler that extends Fastly VCL with programming constructs that generate standard VCL code.
Think of it as a build step for your VCL: write enhanced VCL source files, run xvcl, and get clean, valid VCL output.
Tip: For a quick syntax reference, see the xvcl Quick Reference Guide.
What you can do:
- Define constants once, use them everywhere
- Generate repetitive code with for loops
- Create reusable functions with return values
- Build zero-overhead macros for common patterns
- Conditionally compile code for different environments
- Split large VCL files into modular includes
What you get:
- Standard VCL output that works with Fastly
- Compile-time safety and error checking
- Reduced code duplication
- Better maintainability
Why Use xvcl?
VCL is powerful but limited by design. You can't define functions with return values, you can't use loops, and managing constants means find-and-replace. This leads to:
- Copy-paste errors: Similar backends? Copy, paste, modify, repeat, make mistakes
- Magic numbers: Hardcoded values scattered throughout your code
- Duplication: Same logic repeated in multiple subroutines
- Poor maintainability: Change one thing, update it in 20 places
xvcl solves these problems by adding programming constructs that compile down to clean VCL.
Real-World Example
Without xvcl (manual, error-prone):
backend web1 {
.host = "web1.example.com";
.port = "80";
}
backend web2 {
.host = "web2.example.com";
.port = "80";
}
backend web3 {
.host = "web3.example.com";
.port = "80";
}
sub vcl_recv {
if (req.http.Host == "web1.example.com") {
set req.backend = web1;
}
if (req.http.Host == "web2.example.com") {
set req.backend = web2;
}
if (req.http.Host == "web3.example.com") {
set req.backend = web3;
}
}
With xvcl (clean, maintainable):
#const BACKENDS = ["web1", "web2", "web3"]
#for backend in BACKENDS
backend {{backend}} {
.host = "{{backend}}.example.com";
.port = "80";
}
#endfor
sub vcl_recv {
#for backend in BACKENDS
if (req.http.Host == "{{backend}}.example.com") {
set req.backend = {{backend}};
}
#endfor
}
Adding a new backend? Just update the list. xvcl generates all the code.
Installation
xvcl is a Python package. You need Python 3.9 or later.
# Using pip
pip install xvcl
# Or install from source
pip install .
# Development installation with dev dependencies
pip install -e ".[dev]"
# Using uv (recommended for faster installation)
uv tool install xvcl
After installation, the xvcl command is available globally.
No external dependencies — uses only the Python standard library.
Running without installation
You can run xvcl without installing it using uvx:
# Run xvcl directly (uv will handle dependencies automatically)
uvx xvcl input.xvcl -o output.vcl
# With include paths
uvx xvcl input.xvcl -o output.vcl -I ./includes
# With debug mode
uvx xvcl input.xvcl -o output.vcl --debug
This is perfect for CI/CD pipelines or one-off usage without polluting your environment.
Quick Start
Create an xvcl source file (use the .xvcl extension by convention):
hello.xvcl:
#const MESSAGE = "Hello from xvcl!"
sub vcl_recv {
set req.http.X-Message = "{{MESSAGE}}";
}
Run xvcl:
# If installed
xvcl hello.xvcl -o hello.vcl
# Or run without installing using uvx
uvx xvcl hello.xvcl -o hello.vcl
Output hello.vcl:
sub vcl_recv {
set req.http.X-Message = "Hello from xvcl!";
}
Validate with Falco:
falco lint hello.vcl
Core Features
Constants
Define named constants with type checking. Constants are evaluated at preprocessing time and substituted into your code.
Syntax:
#const NAME TYPE = value
Supported types:
INTEGER- Whole numbersSTRING- Text stringsFLOAT- Decimal numbersBOOL- True/False
Example:
#const MAX_AGE INTEGER = 3600
#const ORIGIN STRING = "origin.example.com"
#const PRODUCTION BOOL = True
#const CACHE_VERSION FLOAT = 1.5
Using constants in templates:
#const TTL = 300
#const BACKEND_HOST = "api.example.com"
backend F_api {
.host = "{{BACKEND_HOST}}";
.port = "443";
}
sub vcl_fetch {
set beresp.ttl = {{TTL}}s;
}
Why use constants?
- Single source of truth for configuration values
- Easy to update across entire VCL
- Type safety prevents mistakes
- Self-documenting code
Template Expressions
Embed Python expressions in your VCL using {{expression}} syntax. Expressions are evaluated at preprocessing time.
Example:
#const PORT = 8080
sub vcl_recv {
set req.http.X-Port = "{{PORT}}";
set req.http.X-Double = "{{PORT * 2}}";
set req.http.X-Hex = "{{hex(PORT)}}";
}
Output:
sub vcl_recv {
set req.http.X-Port = "8080";
set req.http.X-Double = "16160";
set req.http.X-Hex = "0x1f90";
}
Available functions:
range(n)- Generate number sequenceslen(list)- Get list lengthstr(x),int(x)- Type conversionshex(n)- Hexadecimal conversionformat(x, fmt)- Format valuesenumerate(iterable)- Enumerate with indicesmin(...),max(...)- Min/max valuesabs(n)- Absolute value
String formatting:
#const REGION = "us-east"
#const INDEX = 1
set req.backend = F_backend_{{REGION}}_{{INDEX}};
For Loops
Generate repetitive VCL code by iterating over ranges or lists.
Syntax:
#for variable in iterable
// Code to repeat
#endfor
#for var1, var2 in iterable
// Tuple unpacking
#endfor
Example 1: Range-based loop
#for i in range(5)
backend web{{i}} {
.host = "web{{i}}.example.com";
.port = "80";
}
#endfor
Output:
backend web0 {
.host = "web0.example.com";
.port = "80";
}
backend web1 {
.host = "web1.example.com";
.port = "80";
}
// ... continues through web4
Example 2: List iteration
#const REGIONS = ["us-east", "us-west", "eu-west"]
#for region in REGIONS
backend F_{{region}} {
.host = "{{region}}.example.com";
.port = "443";
.ssl = true;
}
#endfor
Example 3: Nested loops
#const REGIONS = ["us", "eu"]
#const ENVS = ["prod", "staging"]
#for region in REGIONS
#for env in ENVS
backend {{region}}_{{env}} {
.host = "{{env}}.{{region}}.example.com";
.port = "443";
}
#endfor
#endfor
Example 4: Tuple unpacking
#const BACKENDS = [("web", 8080), ("api", 9000), ("admin", 9001)]
#for name, port in BACKENDS
backend F_{{name}} {
.host = "{{name}}.example.com";
.port = "{{port}}";
}
#endfor
Example 5: With enumerate
#const REGIONS = ["us-east", "us-west", "eu-west"]
#for idx, region in enumerate(REGIONS)
set req.http.X-Region-{{idx}} = "{{region}}";
#endfor
Why use for loops?
- Generate multiple similar backends
- Create ACL entries from lists
- Build routing logic programmatically
- Reduce copy-paste errors
Conditionals
Conditionally include or exclude code based on compile-time conditions.
Syntax:
#if condition
// Code when true
#else
// Code when false (optional)
#endif
Example 1: Environment-specific configuration
#const PRODUCTION = True
#const DEBUG = False
sub vcl_recv {
#if PRODUCTION
set req.http.X-Environment = "production";
unset req.http.X-Debug-Info;
#else
set req.http.X-Environment = "development";
set req.http.X-Debug-Info = "Enabled";
#endif
#if DEBUG
set req.http.X-Request-ID = randomstr(16, "0123456789abcdef");
#endif
}
Example 2: Feature flags
#const ENABLE_NEW_ROUTING = True
#const ENABLE_RATE_LIMITING = False
sub vcl_recv {
#if ENABLE_NEW_ROUTING
call new_routing_logic;
#else
call legacy_routing_logic;
#endif
#if ENABLE_RATE_LIMITING
if (ratelimit.check_rate("client_" + client.ip, 1, 100, 60s, 1000s)) {
error 429 "Too Many Requests";
}
#endif
}
Why use conditionals?
- Single codebase for multiple environments
- Easy feature flag management
- Dead code elimination (code in false branches isn't generated)
- Compile-time optimization
Variables
Declare and initialize local variables in one step.
Syntax:
#let name TYPE = expression;
Example:
sub vcl_recv {
#let timestamp STRING = std.time(now, now);
#let cache_key STRING = req.url.path + req.http.Host;
set req.http.X-Timestamp = var.timestamp;
set req.hash = var.cache_key;
}
Expands to:
sub vcl_recv {
declare local var.timestamp STRING;
set var.timestamp = std.time(now, now);
declare local var.cache_key STRING;
set var.cache_key = req.url.path + req.http.Host;
set req.http.X-Timestamp = var.timestamp;
set req.hash = var.cache_key;
}
Why use #let?
- Shorter syntax than separate declare + set
- Clear initialization point
- Reduces boilerplate
File Includes
Split large VCL files into modular, reusable components.
Syntax:
#include "path/to/file.xvcl"
Example project structure:
vcl/
├── main.xvcl
├── includes/
│ ├── backends.xvcl
│ ├── security.xvcl
│ └── routing.xvcl
main.xvcl:
#include "includes/backends.xvcl"
#include "includes/security.xvcl"
#include "includes/routing.xvcl"
sub vcl_recv {
call security_checks;
call routing_logic;
}
includes/backends.xvcl:
#const BACKENDS = ["web1", "web2", "web3"]
#for backend in BACKENDS
backend F_{{backend}} {
.host = "{{backend}}.example.com";
.port = "443";
}
#endfor
Include path resolution:
- Relative to the current file
- Relative to include paths specified with
-I
Run with include paths:
xvcl main.xvcl -o main.vcl -I ./vcl/includes
Features:
- Include-once semantics: Files are only included once even if referenced multiple times
- Cycle detection: Prevents circular includes
- Shared constants: Constants defined in included files are available to the parent
Why use includes?
- Organize large VCL projects
- Share common configurations across multiple VCL files
- Team collaboration (different files for different concerns)
- Reusable components library
Advanced Features
Inline Macros
Create zero-overhead text substitution macros. Unlike functions, macros are expanded inline at compile time with no runtime cost.
Syntax:
#inline macro_name(param1, param2, ...)
expression
#endinline
Example 1: String concatenation
#inline add_prefix(s)
"prefix-" + s
#endinline
#inline add_suffix(s)
s + "-suffix"
#endinline
sub vcl_recv {
set req.http.X-Modified = add_prefix("test");
set req.http.X-Both = add_prefix(add_suffix("middle"));
}
Output:
sub vcl_recv {
set req.http.X-Modified = "prefix-" + "test";
set req.http.X-Both = "prefix-" + "middle" + "-suffix";
}
Example 2: Common patterns
#inline normalize_host(host)
std.tolower(regsub(host, "^www\.", ""))
#endinline
#inline cache_key(url, host)
digest.hash_md5(url + "|" + host)
#endinline
sub vcl_recv {
set req.http.X-Normalized = normalize_host(req.http.Host);
set req.hash = cache_key(req.url, req.http.Host);
}
Output:
sub vcl_recv {
set req.http.X-Normalized = std.tolower(regsub(req.http.Host, "^www\.", ""));
set req.hash = digest.hash_md5(req.url + "|" + req.http.Host);
}
Example 3: Operator precedence handling
xvcl automatically handles operator precedence:
#inline double(x)
x + x
#endinline
sub vcl_recv {
declare local var.result INTEGER;
set var.result = double(5) * 10; // Correctly expands to (5 + 5) * 10
}
Macros vs Functions:
| Feature | Macros | Functions |
|---|---|---|
| Expansion | Compile-time inline | Runtime subroutine call |
| Overhead | None | Subroutine call + global vars |
| Return values | Expression only | Single or tuple |
| Use case | Simple expressions | Complex logic |
When to use macros:
- String manipulation patterns
- Simple calculations
- Common expressions repeated throughout code
- When you need zero runtime overhead
When to use functions:
- Complex logic with multiple statements
- Need to return multiple values
- Conditional logic or loops inside the reusable code
Functions
Define reusable functions with parameters and return values. Functions are compiled into VCL subroutines.
Syntax:
#def function_name(param1 TYPE, param2 TYPE, ...) -> RETURN_TYPE
// Function body
return value;
#enddef
Example 1: Simple function
#def add(a INTEGER, b INTEGER) -> INTEGER
declare local var.sum INTEGER;
set var.sum = a + b;
return var.sum;
#enddef
sub vcl_recv {
declare local var.result INTEGER;
set var.result = add(5, 10);
set req.http.X-Sum = var.result;
}
Example 2: String processing
#def normalize_path(path STRING) -> STRING
declare local var.result STRING;
set var.result = std.tolower(path);
set var.result = regsub(var.result, "/$", "");
return var.result;
#enddef
sub vcl_recv {
declare local var.clean_path STRING;
set var.clean_path = normalize_path(req.url.path);
set req.url = var.clean_path;
}
Example 3: Functions with conditionals
#def should_cache(url STRING) -> BOOL
declare local var.cacheable BOOL;
if (url ~ "^/api/") {
set var.cacheable = false;
} else if (url ~ "\.(jpg|png|css|js)$") {
set var.cacheable = true;
} else {
set var.cacheable = false;
}
return var.cacheable;
#enddef
sub vcl_recv {
declare local var.can_cache BOOL;
set var.can_cache = should_cache(req.url.path);
if (var.can_cache) {
return(lookup);
} else {
return(pass);
}
}
Example 4: Tuple returns (multiple values)
#def parse_user_agent(ua STRING) -> (STRING, STRING)
declare local var.browser STRING;
declare local var.os STRING;
if (ua ~ "Chrome") {
set var.browser = "chrome";
} else if (ua ~ "Firefox") {
set var.browser = "firefox";
} else {
set var.browser = "other";
}
if (ua ~ "Windows") {
set var.os = "windows";
} else if (ua ~ "Mac") {
set var.os = "macos";
} else {
set var.os = "other";
}
return var.browser, var.os;
#enddef
sub vcl_recv {
declare local var.browser STRING;
declare local var.os STRING;
set var.browser, var.os = parse_user_agent(req.http.User-Agent);
set req.http.X-Browser = var.browser;
set req.http.X-OS = var.os;
}
Behind the scenes:
Functions are compiled into VCL subroutines using global headers for parameter passing:
// Your code:
set var.result = add(5, 10);
// Becomes:
set req.http.X-Func-add-a = std.itoa(5);
set req.http.X-Func-add-b = std.itoa(10);
call add;
set var.result = std.atoi(req.http.X-Func-add-Return);
xvcl generates the sub add { ... } implementation and handles all type conversions automatically.
Function features:
- Type safety: Parameters and returns are type-checked
- Multiple returns: Use tuple syntax to return multiple values
- Automatic conversions: INTEGER/FLOAT/BOOL are converted to/from STRING automatically
- Scope annotations: Generated subroutines work in all VCL scopes
Why use functions?
- Reusable complex logic
- Reduce code duplication
- Easier testing (test the function once)
- Better code organization
Command-Line Usage
Basic usage:
xvcl input.xvcl -o output.vcl
Options:
| Option | Description |
|---|---|
input |
Input xvcl source file (required) |
-o, --output |
Output VCL file (default: replaces .xvcl with .vcl) |
-I, --include |
Add an include search path (repeatable) |
--debug |
Enable debug output with expansion traces |
--source-maps |
Add source map comments to output |
-v, --verbose |
Verbose output (alias for --debug) |
--error-format |
Error output format: text (default) or json |
Examples:
# Basic compilation
xvcl main.xvcl -o main.vcl
# With include paths
xvcl main.xvcl -o main.vcl \
-I ./includes \
-I ./shared
# Debug mode (see expansion traces)
xvcl main.xvcl -o main.vcl --debug
# With source maps (track generated code origin)
xvcl main.xvcl -o main.vcl --source-maps
Automatic output naming:
If you don't specify -o, xvcl replaces .xvcl with .vcl:
# These are equivalent:
xvcl main.xvcl
xvcl main.xvcl -o main.vcl
Debug mode output:
$ xvcl example.xvcl --debug
[DEBUG] Processing file: example.xvcl
[DEBUG] Pass 1: Extracting constants
[DEBUG] Defined constant: MAX_AGE = 3600
[DEBUG] Pass 2: Processing includes
[DEBUG] Pass 3: Extracting inline macros
[DEBUG] Defined macro: add_prefix(s)
[DEBUG] Pass 4: Extracting functions
[DEBUG] Pass 5: Processing directives and generating code
[DEBUG] Processing #for at line 10
[DEBUG] Loop iterating 3 times
[DEBUG] Iteration 0: backend = web1
[DEBUG] Iteration 1: backend = web2
[DEBUG] Iteration 2: backend = web3
[DEBUG] Pass 6: Generating function subroutines
✓ Compiled example.xvcl -> example.vcl
Constants: 1
Macros: 1 (add_prefix)
Functions: 0
Integration with Falco
xvcl generates standard VCL that you can use with Falco's full toolset.
Recommended workflow:
# 1. Write your xvcl source
vim main.xvcl
# 2. Compile with xvcl
xvcl main.xvcl -o main.vcl
# 3. Lint with Falco
falco lint main.vcl
# 4. Test with Falco
falco test main.vcl
# 5. Simulate with Falco
falco simulate main.vcl
Makefile integration:
# Makefile
.PHONY: build lint test clean
XVCL = xvcl
SOURCES = $(wildcard *.xvcl)
OUTPUTS = $(SOURCES:.xvcl=.vcl)
build: $(OUTPUTS)
%.vcl: %.xvcl
$(XVCL) $< -o $@ -I ./includes
lint: build
falco lint *.vcl
test: build
falco test *.vcl
clean:
rm -f $(OUTPUTS)
CI/CD integration:
# .github/workflows/vcl.yml
name: VCL CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install uv
uses: astral-sh/setup-uv@v2
- name: Install Falco
run: |
wget https://github.com/ysugimoto/falco/releases/latest/download/falco_linux_amd64
chmod +x falco_linux_amd64
sudo mv falco_linux_amd64 /usr/local/bin/falco
- name: Compile xvcl
run: |
uvx xvcl main.xvcl -o main.vcl
- name: Lint VCL
run: falco lint main.vcl
- name: Test VCL
run: falco test main.vcl
Using uvx eliminates the need to set up Python and install xvcl separately — it's handled automatically.
Testing compiled VCL:
You can write Falco tests for your generated VCL:
main.test.vcl:
// @suite: Backend routing tests
// @test: Should route to correct backend
sub test_backend_routing {
set req.http.Host = "web1.example.com";
call vcl_recv;
assert.equal(req.backend, "web1");
}
Run tests after compilation:
xvcl main.xvcl -o main.vcl
falco test main.vcl
Best Practices
1. Use the .xvcl extension
Makes it clear which files are xvcl source files:
✓ main.xvcl → main.vcl
✗ main.vcl → main.vcl.processed
2. Keep constants at the top
// Good: Constants first, easy to find
#const MAX_BACKENDS = 10
#const PRODUCTION = True
#for i in range(MAX_BACKENDS)
// ... use constant
#endfor
3. Use descriptive constant names
// Good
#const CACHE_TTL_SECONDS = 3600
#const API_BACKEND_HOST = "api.example.com"
// Bad
#const X = 3600
#const B = "api.example.com"
4. Comment your macros and functions
// Normalizes a hostname by removing www prefix and converting to lowercase
#inline normalize_host(host)
std.tolower(regsub(host, "^www\.", ""))
#endinline
// Parses User-Agent and returns (browser, os) tuple
#def parse_user_agent(ua STRING) -> (STRING, STRING)
// ...
#enddef
5. Prefer macros for simple expressions, functions for complex logic
// Good: Simple expression = macro
#inline cache_key(url, host)
digest.hash_md5(url + "|" + host)
#endinline
// Good: Complex logic = function
#def should_cache(url STRING, method STRING) -> BOOL
declare local var.result BOOL;
if (method != "GET" && method != "HEAD") {
set var.result = false;
} else if (url ~ "^/api/") {
set var.result = false;
} else {
set var.result = true;
}
return var.result;
#enddef
6. Use includes for organization
vcl/
├── main.xvcl # Main entry point
├── config.xvcl # Constants and configuration
├── includes/
│ ├── backends.xvcl
│ ├── security.xvcl
│ ├── routing.xvcl
│ └── caching.xvcl
7. Version control both source and output
# Include both in git
*.xvcl
*.vcl
# But gitignore generated files in CI
# (if you regenerate on deploy)
Or only version control source files and regenerate on deployment:
# Version control xvcl source only
*.xvcl
# Ignore generated VCL
*.vcl
Choose based on your deployment process.
8. Add source maps in development
# Development: easier debugging
xvcl main.xvcl -o main.vcl --source-maps
# Production: cleaner output
xvcl main.xvcl -o main.vcl
Source maps add comments like:
// BEGIN INCLUDE: includes/backends.xvcl
backend F_web1 { ... }
// END INCLUDE: includes/backends.xvcl
9. Test incrementally
Don't write a massive source file and compile once. Test as you go:
# Write a bit
vim main.xvcl
# Compile
xvcl main.xvcl
# Check output
cat main.vcl
# Lint
falco lint main.vcl
# Repeat
10. Use debug mode when things go wrong
xvcl main.xvcl --debug
Shows exactly what xvcl is doing.
Troubleshooting
Error: "Name 'X' is not defined"
Problem: You're using a variable or constant that doesn't exist.
#const PORT = 8080
set req.http.X-Value = "{{PROT}}"; // Typo!
Error:
Error at main.xvcl:3:
Name 'PROT' is not defined
Did you mean: PORT?
Solution: Check spelling. xvcl suggests similar names.
Error: "Invalid #const syntax"
Problem: Malformed constant declaration.
#const PORT 8080 // Missing = sign
#const = 8080 // Missing name
#const PORT = STRING // Missing value
Solution: Use correct syntax:
#const PORT INTEGER = 8080
Error: "No matching #endfor for #for"
Problem: Missing closing keyword.
#for i in range(10)
backend web{{i}} { ... }
// Missing #endfor
Solution: Add the closing keyword:
#for i in range(10)
backend web{{i}} { ... }
#endfor
Error: "Circular include detected"
Problem: File A includes file B which includes file A.
main.xvcl includes util.xvcl
util.xvcl includes main.xvcl
Solution: Restructure your includes. Create a shared file:
main.xvcl includes shared.xvcl
util.xvcl includes shared.xvcl
Error: "Cannot find included file"
Problem: Include path is wrong or file doesn't exist.
#include "includes/backends.xvcl"
Solution: Check path and use -I flag:
xvcl main.xvcl -o main.vcl -I ./includes
Generated VCL has syntax errors
Problem: xvcl generated invalid VCL.
Solution:
-
Check the generated output:
cat main.vcl -
Find the problematic section
-
Trace back to source with
--source-maps:xvcl main.xvcl -o main.vcl --source-maps
-
Fix the source file
Macro expansion issues
Problem: Macro expands incorrectly.
#inline double(x)
x + x
#endinline
set var.result = double(1 + 2);
// Expands to: (1 + 2) + (1 + 2) ✓ Correct
xvcl automatically adds parentheses when needed.
If you see issues: Check operator precedence in your macro definition.
Function calls not working
Problem: Function call doesn't get replaced.
#def add(a INTEGER, b INTEGER) -> INTEGER
return a + b;
#enddef
set var.result = add(5, 10);
Common causes:
-
Missing semicolon: Function calls must end with
;set var.result = add(5, 10); // ✓ Correct set var.result = add(5, 10) // ✗ Won't match
-
Wrong number of arguments:
set var.result = add(5); // ✗ Expects 2 args
-
Typo in function name:
set var.result = addr(5, 10); // ✗ Function 'addr' not defined
Performance issues
Problem: Compilation is slow.
Common causes:
- Large loops:
#for i in range(10000)generates 10,000 copies - Deep nesting: Multiple nested loops or includes
- Complex macros: Heavily nested macro expansions
Solutions:
- Reduce loop iterations if possible
- Use functions instead of generating everything inline
- Split into multiple source files
- Profile with
--debugto see what's slow
Getting help
Check the error context:
Errors show surrounding lines:
Error at main.xvcl:15:
Invalid #for syntax: #for in range(10)
Context:
13: sub vcl_recv {
14: // Generate backends
→ 15: #for in range(10)
16: backend web{{i}} { ... }
17: #endfor
18: }
Enable debug mode:
xvcl main.xvcl --debug
Validate generated VCL:
falco lint main.vcl -vv
The -vv flag shows detailed Falco errors.
Examples Gallery
Here are complete, working examples you can use as starting points.
Example 1: Multi-region backends
multi-region.xvcl:
#const REGIONS = ["us-east", "us-west", "eu-west", "ap-south"]
#const DEFAULT_REGION = "us-east"
#for region in REGIONS
backend F_origin_{{region}} {
.host = "origin-{{region}}.example.com";
.port = "443";
.ssl = true;
.connect_timeout = 5s;
.first_byte_timeout = 30s;
.between_bytes_timeout = 10s;
}
#endfor
sub vcl_recv {
declare local var.region STRING;
// Detect region from client IP or header
if (req.http.X-Region) {
set var.region = req.http.X-Region;
} else {
set var.region = "{{DEFAULT_REGION}}";
}
// Route to appropriate backend
#for region in REGIONS
if (var.region == "{{region}}") {
set req.backend = F_origin_{{region}};
}
#endfor
}
Example 2: Feature flag system
feature-flags.xvcl:
#const ENABLE_NEW_CACHE_POLICY = True
#const ENABLE_WEBP_CONVERSION = True
#const ENABLE_ANALYTICS = False
#const ENABLE_DEBUG_HEADERS = False
sub vcl_recv {
#if ENABLE_NEW_CACHE_POLICY
// New cache policy with fine-grained control
if (req.url.path ~ "\.(jpg|png|gif|css|js)$") {
set req.http.X-Cache-Policy = "static";
} else {
set req.http.X-Cache-Policy = "dynamic";
}
#else
// Legacy cache policy
set req.http.X-Cache-Policy = "default";
#endif
#if ENABLE_WEBP_CONVERSION
if (req.http.Accept ~ "image/webp") {
set req.http.X-Image-Format = "webp";
}
#endif
#if ENABLE_ANALYTICS
set req.http.X-Analytics-ID = uuid.generate();
#endif
}
sub vcl_deliver {
#if ENABLE_DEBUG_HEADERS
set resp.http.X-Cache-Status = resp.http.X-Cache;
set resp.http.X-Backend = req.backend;
set resp.http.X-Region = req.http.X-Region;
#endif
}
Example 3: URL normalization library
url-utils.xvcl:
// Inline macros for common URL operations
#inline strip_www(host)
regsub(host, "^www\.", "")
#endinline
#inline lowercase_host(host)
std.tolower(host)
#endinline
#inline normalize_host(host)
lowercase_host(strip_www(host))
#endinline
#inline remove_trailing_slash(path)
regsub(path, "/$", "")
#endinline
#inline remove_query_string(url)
regsub(url, "\?.*$", "")
#endinline
// Function for complex normalization
#def normalize_url(url STRING, host STRING) -> STRING
declare local var.result STRING;
declare local var.clean_host STRING;
declare local var.clean_path STRING;
set var.clean_host = normalize_host(host);
set var.clean_path = remove_trailing_slash(url);
set var.result = "https://" + var.clean_host + var.clean_path;
return var.result;
#enddef
sub vcl_recv {
declare local var.canonical_url STRING;
set var.canonical_url = normalize_url(req.url.path, req.http.Host);
set req.http.X-Canonical-URL = var.canonical_url;
}
Example 4: Lookup table generation
lookup-tables.xvcl:
#const HEX_DIGITS = "0123456789abcdef"
// Generate a byte-to-hex lookup table
table byte_to_hex STRING {
#for i in range(256)
"{{i}}": "{{format(i, '02x')}}"{{", " if i < 255 else ""}}
#endfor
}
// Generate HTTP status code descriptions
#const STATUS_CODES = [200, 201, 301, 302, 400, 401, 403, 404, 500, 502, 503]
#const STATUS_MESSAGES = ["OK", "Created", "Moved Permanently", "Found", "Bad Request", "Unauthorized", "Forbidden", "Not Found", "Internal Server Error", "Bad Gateway", "Service Unavailable"]
table status_messages STRING {
#for i in range(len(STATUS_CODES))
"{{STATUS_CODES[i]}}": "{{STATUS_MESSAGES[i]}}"{{", " if i < len(STATUS_CODES) - 1 else ""}}
#endfor
}
sub vcl_deliver {
// Use generated tables
set resp.http.X-Status-Message = table.lookup(status_messages, resp.status);
}
Summary
xvcl extends Fastly VCL with powerful programming constructs:
Core features:
- Constants - Single source of truth for configuration
- Template expressions - Dynamic value substitution
- For loops - Generate repetitive code
- Conditionals - Environment-specific builds
- Variables - Cleaner local variable syntax
- Includes - Modular code organization
Advanced features:
- Inline macros - Zero-overhead text substitution
- Functions - Reusable logic with return values
Benefits:
- Less code duplication
- Fewer copy-paste errors
- Better maintainability
- Easier testing
- Faster development
Integration:
- Works with Falco's full toolset
- Standard VCL output
- No runtime overhead
- Easy CI/CD integration
Start simple, add complexity as needed. xvcl grows with your VCL projects.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xvcl-2.8.1.tar.gz.
File metadata
- Download URL: xvcl-2.8.1.tar.gz
- Upload date:
- Size: 52.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4f5facdbcafcc86131f65d305e28ee8b0e1946aad5ce888250f4eb88d1aa367f
|
|
| MD5 |
66e64506f5bf0d37374bef4711af0e6c
|
|
| BLAKE2b-256 |
66b4ffc1f8698e7e6b1e7fa13ba08fd2250b207079dea12c0a4d526f9977dfcc
|
File details
Details for the file xvcl-2.8.1-py3-none-any.whl.
File metadata
- Download URL: xvcl-2.8.1-py3-none-any.whl
- Upload date:
- Size: 32.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1a2de596765d2eab67a71c114486310835e61eb52f4aaa994071221eddb0e2a2
|
|
| MD5 |
08099fe2ad67f579346edd3c3fc1d8f2
|
|
| BLAKE2b-256 |
a0abc5e2d9071eca66dae5696d1f7271710ea89e6329a965d0dc3aab8cb80ed8
|