← Files Fastly Agent ToolkitARCHIVED FILE
skills/fastly-fiddle/SKILL.md
16.7 KB · Oct 10, 2026 · 18:29 UTC
---
name: fastly-fiddle
description: "Use when testing VCL against real Fastly edge infrastructure, writing assertion-based Fiddle tests, producing shareable fiddle URLs for bug reproductions, running VCL integration tests in CI, linting VCL remotely via the Fiddle API, or working with clientFetch/events/originFetches test expressions."
---
# Fastly Fiddle — Real-Edge VCL Testing
## Trigger and scope
Trigger on: Fastly Fiddle, fiddle.fastly.dev URLs, the Fiddle HTTP API, CI testing of VCL services, real-edge VCL tests, shareable VCL reproductions, `clientFetch.*`/`events.*`/`originFetches.*` test expressions, `.test.js` / Mocha specs that target fiddles, SSE `updateResult` / `waitingForSync` events, remote VCL linting via Fiddle, or validating real Fastly behavior (geo, WAF, ESI, clustering, shielding) that local tools cannot simulate.
Do NOT use for: local VCL unit testing (use `falco`), fast TDD loops (Fiddle has a 10-20s edge-sync floor per publish), Fastly Compute/Wasm testing (use `viceroy` or `fastlike`), production service deployment (use `fastly-cli`), or anything requiring authenticated Fastly API access — Fiddle does not use your Fastly API key.
Fastly Fiddle is a web-based sandbox at <https://fiddle.fastly.dev> that compiles and runs VCL on real Fastly edge nodes. Because it uses the production VCL compiler and real POPs, it's the only way outside a real service to test VCL features that depend on edge infrastructure — geolocation data, WAF, ESI, clustering, shielding, rate limiting, real TLS, and real cache behavior.
**Official UI**: <https://fiddle.fastly.dev>
**Demo CI runner**: <https://github.com/fastly/demo-fiddle-ci>
**API base**: `https://fiddle.fastly.dev` (undocumented but stable; no auth required for public fiddles)
## Prerequisites and public uploads
The helper requires Bash, `curl` 7.76 or newer, `jq`, and network access to `fiddle.fastly.dev`.
If a tool is missing, name it and provide installation guidance before running the helper.
Creating or updating a fiddle uploads its VCL, origins, request headers, request bodies, and tests to a public service, including with `--lint-only`.
Only publish when the user has authorized a public Fiddle upload or shareable reproduction.
For a request limited to local testing, use Falco and keep the spec local.
Never include credentials or private project data in a fiddle.
Set `FIDDLE_SKILL_DIR` to the absolute directory containing this `SKILL.md`, using the installed skill location supplied by the client:
```bash
FIDDLE_SKILL_DIR=/absolute/path/to/fastly-fiddle
```
This is a variable you assign, not a client-provided environment variable.
Keep the user's project as the working directory so inputs such as `fiddle.json` and `spec.json` remain project-relative.
Bundled scripts and examples use `FIDDLE_SKILL_DIR` instead.
## When Fiddle, when Falco
| Need | Use |
| ----------------------------------------------------- | ---------------------------------- |
| Fast local iteration (< 1s), watch mode, offline | `falco test` |
| Real Fastly VCL compiler and semantics | Fiddle |
| Real `client.geo.*`, WAF, ESI, rate limiting, shield | Fiddle |
| Shareable URL for bug repros and support tickets | Fiddle |
| CI against real edge nodes | Fiddle |
| Structured lint with line/col (no execution required) | Either |
| Fastly Compute (WASM) | Neither — use `viceroy`/`fastlike` |
Common workflow: iterate locally with `falco test` for speed, then push edge cases to Fiddle when you need real Fastly behavior or a shareable link. See [falco-vs-fiddle.md](references/falco-vs-fiddle.md) for full trade-offs.
## Workflow: deliverable first, then the cheapest check that answers your question
Two rules keep you out of the slow path, which is what wastes time and gets
agents killed by a wall-clock limit:
1. **If your job is to produce a spec file, write it to disk first**, before
any network call. The file is the deliverable — it must exist even if a
later publish stalls on a cold edge-sync. Don't build the spec only inside
a `curl --data` argument and lose it when the call blocks.
2. **Match the check to the question.** "Does this VCL compile / is the spec
shape accepted?" is answered by a _single_ `POST /fiddle` reading `valid` —
~1-3s, no execution, no edge-sync wait (see gotcha #5). You only need the
full publish → execute → SSE round trip when you must observe _runtime_
assertion results (actual `clientFetch`/`originFetches`/`events` values),
and that pays the 10-120s edge-sync floor per publish. **Executing is the
exception, not the default** — reach for it deliberately, and always bound
your wait; never block indefinitely on the stream.
### Lint-only (the common case): compile check, no execution
```bash
bash "$FIDDLE_SKILL_DIR/scripts/run-fiddle.sh" --lint-only fiddle.json
# {fiddle_id, url, valid, lintStatus}. Exit 0 = compiles, 2 = lint error
# (details on stderr). No /execute, no SSE, no edge-sync wait.
```
The raw equivalent is one call — POST and read `valid`:
```bash
UA='fiddle-skill-example/1.0'
curl -sS --max-time 30 -X POST https://fiddle.fastly.dev/fiddle \
-H 'Content-Type: application/json' -H "User-Agent: $UA" \
--data @fiddle.json | jq '{valid, lintStatus}' # valid==true ⇒ it compiles
```
### Full round trip (only when you need runtime results)
When you genuinely need to see assertions pass/fail on the edge, the bundled
helper handles publish → execute → SSE → completion-detection in one call,
with a bounded `--max-wait` (default 180s per attempt) so it can't hang:
```bash
bash "$FIDDLE_SKILL_DIR/scripts/run-fiddle.sh" "$FIDDLE_SKILL_DIR/examples/robots.json"
# Prints fiddle URL, then pass/fail JSON per assertion (including body_preview
# and status by default). Exits non-zero on failure. Pass --no-bodies for
# compact output.
# Iterate against an already-published fiddle without paying edge-sync again:
bash "$FIDDLE_SKILL_DIR/scripts/run-fiddle.sh" --id <fiddle-id> # re-execute (warmest, ~2s)
bash "$FIDDLE_SKILL_DIR/scripts/run-fiddle.sh" --id <fiddle-id> spec.json # PUT then execute
```
The equivalent raw-curl flow, for reference or when the helper isn't available:
```bash
UA='fiddle-skill-example/1.0'
# 1. Create. Capture the ID and the validity flag.
RESP=$(curl -sS -X POST https://fiddle.fastly.dev/fiddle \
-H 'Content-Type: application/json' -H 'Accept: application/json' \
-H "User-Agent: $UA" \
--data '{
"origins": ["https://http-me.fastly.dev"],
"vcl": {
"init": "# Synthetic robots.txt response\n# via error restart pattern",
"recv": "if (req.url.path == \"/robots.txt\") {\n error 601;\n}",
"error": "if (obj.status == 601) {\n set obj.status = 200;\n synthetic {\"User-agent: BadBot\"};\n return(deliver);\n}"
},
"requests": [
{ "path": "/robots.txt",
"headers": "X-Custom: value\nX-Other: second",
"tests": ["clientFetch.status is 200", "clientFetch.bodyPreview includes \"BadBot\""] }
]
}')
FID=$(echo "$RESP" | jq -r '.fiddle.id')
echo "$RESP" | jq '{valid, lintStatus}' # bail here if valid is false
# 2. Execute. Subscribe to the SSE stream IMMEDIATELY — session IDs expire fast.
SID=$(curl -sS -X POST "https://fiddle.fastly.dev/fiddle/$FID/execute?cacheID=1" \
-H 'Accept: application/json' -H "User-Agent: $UA" | jq -r '.sessionID')
# 3. Stream results. Emits repeated `event: waitingForSync` (~10-20s on first
# publish) then `event: updateResult` with full pass/fail data. ALWAYS cap
# the stream with --max-time so a slow/cold sync can't block you forever;
# on timeout, re-execute the same $FID (warm, ~2s) rather than re-publishing.
curl -sS -N --max-time 120 -H "User-Agent: $UA" "https://fiddle.fastly.dev/results/$SID/stream"
```
Full protocol details in [api.md](references/api.md).
## Wire-format gotchas
Non-obvious behavior that will break tools round-tripping fiddles programmatically:
1. **`vcl` on input, `src` on output.** You `POST {"vcl": {"recv": "..."}}` but `GET` returns `{"src": {"recv": "..."}}`. The server renames the key on normalization. Any tool that fetches a fiddle and re-publishes it must map `src` → `vcl` (or send `src` — both work on input). **Update existing fiddles with `PUT /fiddle/:id`** — same body shape as `POST`, but partial updates are not supported; omitted subroutines are cleared.
2. **`tests` is a string on the wire.** You can send `tests: ["a", "b"]` but a subsequent `GET` returns `tests: "a\nb"`. One assertion per line. Split on `\n` when reading.
3. **`headers` is a newline-joined string, not an array.** Unlike `tests` (which accepts both), `headers` must be a string: `"headers": "User-Agent: BadBot/1.0\nX-Custom: value"`. An array will be rejected with a validation error.
4. **Request fields are auto-defaulted by the server.** A `GET` of a fiddle you just created will include fields you didn't send: `method: "GET"`, `connType: "h2"` (HTTP/2 by default — matters for tests that depend on protocol), `enableCluster: true`, `enableShield: false`, `useFreshCache: false`, `sourceIP: "client"`, `followRedirects: false`, `delay: 0`. Set them explicitly if you care.
5. **Invalid VCL still gets a fiddle ID.** `POST` returns `{valid: false, lintStatus: {...}, fiddle: {id, ...}}` for broken VCL. On a **create/update** (`POST`/`PUT`) response, `valid` is the lint result — `true` if the VCL compiles, `false` if not, with details in `lintStatus`. That's the number to trust for "does this compile?", and you get it without executing anything. Don't rely on HTTP status.
**But `valid` means something different on a `GET`.** There it tracks execution, not compilation: it stays `false` until the fiddle has been executed at least once, then flips to `true`. A fiddle that lints perfectly cleanly still reads back as `valid: false` right after you create it. So judge compilation from the create/update response (or by re-submitting the spec) — never from a `GET`. A `GET` can't distinguish a valid-but-not-yet-run fiddle from a genuinely broken one: both come back `valid: false` with an empty `lintStatus`.
6. **VCL string concat with `+` rejects parenthesized operands.** `set X = "used=" + (a - b);` fails with a _misleading_ "Remove the trailing `+` operator" suggestion — the `+` is fine, the `(` is what the parser rejects. Compute the sub-expression into a local variable first. See [spec-shape.md](references/spec-shape.md#vcl-string-concatenation).
7. **`error 8NN;` / `error 9NN;` is rejected by Fiddle lint — use 6xx.** Any 800–999 code fails with "8xx and 9xx error codes are used internally by Fastly. Use 6xx instead.", in any subroutine and any context (bare or inside `if`/`else`/`switch`). So the classic `error 801 <url>;` redirect idiom trips Fiddle lint even inside an `if` — use `error 602 "<url>";` and build the 301 in `vcl_error`. Codes 400–799 are accepted. See [spec-shape.md](references/spec-shape.md#error-codes-use-6xx-for-synthetics).
8. **Some test expressions have built-in delays.** `originFetches.count() is 0` returns `asyncDelay: 2500` — the server waits 2.5s before evaluating "did nothing happen?". Client wait time must accommodate this; 45-60s is a safe ceiling.
9. **SSE session IDs are short-lived.** Subscribe to `/results/<sessionID>/stream` within seconds of receiving the ID from `/execute`. Delayed connections get a 404. Always have the stream open before you start waiting on results.
10. **Test DSL has unusual syntax.** No `.first()`, no `reqHeaderValue()`, no `isnt`/`empty` operators. Event objects expose only `url`/`method`/`return`/`status`/`ttl` — not arbitrary `req.http.*` values. **Read [test-dsl.md](references/test-dsl.md) before writing assertions.**
11. **The server assigns a fresh fiddle ID on every `POST /fiddle`** — even for byte-identical input. IDs are not a content hash; back-to-back POSTs of the same body return different IDs, and each new ID needs its own edge-sync pass before `/execute` can produce results. **`PUT /fiddle/<id>`** keeps the URL stable (good for shared bug-repro links) but the new VCL still recompiles and propagates, so PUTs pay the same edge-sync cost as POSTs. The genuinely warm path is **re-executing an unchanged fiddle ID** — same content, repeat `/execute` calls finish in ~2s. Capture the ID from the first POST and reuse it; vary `cacheID` to force cold caches without re-publishing. (PUT is not partial — omitted subroutines are cleared, see #1.)
12. **`originFetches.count() is N` is fragile under retries and shared `cacheID`.** A retry — automatic in `run-fiddle.sh`, or manual via `--id <fid>` — re-executes against the same `cacheID`, so any origin response cached on the previous attempt is now a HIT and `originFetches.count()` drops to 0. Two reliable fixes: set **`useFreshCache: true`** on the request (forces a fresh cache, ignoring the session `cacheID` — see [spec-shape.md `Request objects`](references/spec-shape.md#request-objects)), or assert via **`events.where(fnName=fetch).count()`** (counts subroutine entries, not network calls). The same goes for `originFetches[0].*` assertions whenever the test runs after a possible warmup.
## Authoring conventions
Fiddles are read by humans in a browser. These aren't surprises, but they make shared fiddles useful:
- **Set a `title`.** Makes fiddles findable in browser tabs, bookmarks, and shared links. Example: `"title": "fastly_info.state: compound values deep dive"`.
- **Use `init` as a header comment.** The `init` subroutine renders first in the UI. Put a short summary there (~55 chars/line): `"init": "# fastly_info.state: compound values\n# Demonstrates MISS-CLUSTER, HIT-CLUSTER, HIT-SYNTH"`.
- **Format VCL with `\n` and indentation** rather than cramming everything onto one line.
- **Send `User-Agent: <tool>/<version>` on every API call.** Fiddle is unauthenticated shared infra; default `curl/x.y` or library UAs are bad citizenship. This is the API call's UA, not the simulated request's `headers`.
## Testing in CI
Reference implementation: [fastly/demo-fiddle-ci](https://github.com/fastly/demo-fiddle-ci) — a Node + Mocha harness. Clone it and write your `{spec, scenarios[]}`.
The one non-obvious thing: it publishes the fiddle once, then re-executes the same fiddle ID per scenario with different `requests[]`. Only re-execution is warm (~2s); a fresh publish pays the 10-20s edge-sync floor (see "Limits" below and gotcha #11). Keep scenarios sequential.
## References
| Topic | File | Use when... |
| ------------------- | --------------------------------------------------- | ------------------------------------------------------------------ |
| **Helper script** | [scripts/run-fiddle.sh](scripts/run-fiddle.sh) | Publishing + executing + streaming a fiddle in one shell command |
| **Example payload** | [examples/robots.json](examples/robots.json) | Starting from a known-good minimal fiddle spec |
| **HTTP API** | [api.md](references/api.md) | Calling Fiddle endpoints directly, driving it from any language |
| Fiddle spec shape | [spec-shape.md](references/spec-shape.md) | Building the JSON payload: origins, src, requests, defaults |
| Test DSL | [test-dsl.md](references/test-dsl.md) | Writing `clientFetch.*`, `events.where(...)`, `originFetches.*` |
| Falco vs Fiddle | [falco-vs-fiddle.md](references/falco-vs-fiddle.md) | Choosing the right tool, or combining them in one workflow |
## Limits and cautions
- **No auth required, no quota documented.** Be a good citizen: don't hammer the API in tight loops, and always send a descriptive `User-Agent` on API calls (see Authoring conventions). Use `cacheID` consistently across requests that need to share cache, and vary it to force cold caches.
- **Edge-sync floor is ~10-20s per publish, sometimes much longer.** Applies per fiddle ID, not per unique content — and every `POST /fiddle` mints a new ID (see Wire-format gotchas #11), so re-publishing identical input still pays the full sync cost. Cold publishes regularly take 60-120s in practice. Unusable for TDD. Batch changes; execute once per meaningful delta; reuse IDs via `PUT` when iterating.
- **Execution hops through a real POP** (tests observed running from IAD on node `kiad7000140`). Geographic assertions reflect wherever the fiddle executor landed.
- **Fiddles are public by default.** Don't put secrets in VCL you publish.
- **The API is undocumented.** Field names and behavior can change.
SHA-256: 66e21ab30fed478f36ab0f8418090d54b656dbb32ebd9efd89c3690a0f1bba2e