---
name: aem-dispatcher
description: Writes, reviews and debugs AEM Dispatcher and CDN configuration — flexible-mode file layout, deny-by-default filters, cache rules, statfileslevel and invalidation, TTLs, rewrites and redirects, headers and CORS, and Cloud Service CDN rules for traffic filtering and WAF. Use this whenever the user edits or reviews dispatcher .any, .farm, .vhost, .rules or .vars files (filters.any, rules.any, clientheaders.any, virtualhosts.any, rewrite.rules), runs the dispatcher SDK validator or docker_run.sh, asks why a page is not cached or not invalidated, sees a 404 from dispatcher, exposes .json or .model.json, configures redirects, vanity URLs, CORS or security headers, or edits cdn.yaml. Also apply it when a performance or security issue on AEM publish is really a cache or filter problem.
metadata:
  technology: AEM
  type: architecture
---

# AEM Dispatcher and CDN

Dispatcher is both the security perimeter and the cache for AEM publish. Every rule must answer two questions: *can an attacker reach something they shouldn't through this?* and *will this response be cached and correctly invalidated?* Deny by default, cache by default, and prove both with the validator and response headers.

## 1. File layout (Cloud Service, flexible mode)

```
dispatcher/src/
├── opt-in/USE_SOURCES_DIRECTLY          ← enables flexible mode
├── conf.d/
│   ├── available_vhosts/<site>.vhost    ← your vhosts
│   ├── enabled_vhosts/<site>.vhost      ← symlink → ../available_vhosts/<site>.vhost
│   ├── rewrites/rewrite.rules           ← mod_rewrite rules (included from vhost)
│   └── variables/custom.vars            ← Define ${CONTENT_FOLDER_NAME} etc.
└── conf.dispatcher.d/
    ├── available_farms/<site>.farm       enabled_farms/<site>.farm → symlink
    ├── filters/filters.any              ← $include "./default_filters.any" + yours
    ├── cache/rules.any                  ← cacheable paths
    ├── clientheaders/clientheaders.any  ← headers passed to publish
    ├── virtualhosts/virtualhosts.any    ← hostnames for the farm
    └── renders/                          ← publish render (Adobe-managed default)
```

- **Immutable files** (overwritten or rejected by the pipeline): the `default_*` files (`default_filters.any`, `default_rules.any`, `default_invalidate.any`, `default_clientheaders.any`, `default_virtualhosts.any`, `default_renders.any`, `default.farm`, `default.vhost`), `dispatcher.any`, `dispatcher_vhost.conf`, and the Apache base config. Extend them via `$include` and your own files; treat the validator's list as authoritative.
- Symlinks must be real relative symlinks (Windows checkouts break them).
- Environment differences via Apache variables (`${ENVIRONMENT_TYPE}` and `Define`s), not per-environment file copies.

**Validate before every commit:**

```bash
./bin/validate.sh ./src                                   # syntax, immutability, allowed directives
./bin/docker_run.sh ./src host.docker.internal:4503 8080  # run against local publish SDK
```

On 6.5 the same principles apply, but you own the whole `dispatcher.any` and the Apache build.

## 2. Filters: allow-list only

Filters are evaluated top to bottom; **last match wins**. Start from deny-all (the Cloud defaults already do) and add narrow allows.

```
/0100 { /type "allow" /method "GET" /path "/content/mysite/*" /extension "html" }
/0101 { /type "allow" /method "GET" /path "/content/mysite/*" /selectors "model" /extension "json" }
/0102 { /type "allow" /method "GET" /url "/graphql/execute.json/mysite/*" }   # persisted queries, exact prefix
/0110 { /type "deny"  /selectors "(feed|rss|pages|languages|blueprint|infinity|tidy|sysview|docview|query|[0-9-]+)" /extension "json" }
/0111 { /type "deny"  /path "/content/*" /query "debug=*" }
```

| Rule | Why |
|---|---|
| Match on `/path`, `/selectors`, `/extension`, `/suffix`, `/method`, `/query` — not a raw `/url` glob | Glob URLs are bypassable with selectors/suffixes |
| Never allow `/crx/*`, `/system/*`, `/bin/*`, `/libs/*` broadly | Consoles, Query Builder, servlets; allow one exact servlet path if needed |
| `.json` only with an explicit selector (`model`) and path | Default JSON servlet leaks content structure; `.infinity.json` dumps trees |
| Deny numeric selectors (`.1.json`, `.-1.json`) | Recursive JSON rendering |
| POST only to exact form-handler paths | Everything else on publish is read-only |
| Test the negative cases | A filter review isn't done until blocked URLs return 404 |

## 3. Cache rules

- `/rules`: cache `*` under content paths; deny only what must never be cached (authenticated or per-user endpoints).
- **`/ignoreUrlParams`** — ignore everything except parameters that change the response; otherwise every tracking parameter (`utm_*`, `gclid`) is a cache miss:

```
/ignoreUrlParams {
  /0001 { /glob "*"    /type "allow" }   # allow = ignore for caching
  /0002 { /glob "page" /type "deny"  }   # deny = response depends on it → not cached
}
```

- **`/statfileslevel`**: set to the depth of your site roots (e.g. `/content/brand/country/lang` → 4 or 5) so publishing a page in one site doesn't invalidate every site. Too low → mass invalidation; too high → stale navigation/header fragments shared across the tree.
- **`/invalidate`**: auto-invalidate `*.html` (and page `.json`) on activation; don't auto-invalidate clientlibs or DAM renditions — they use versioned/fingerprinted URLs.
- **`/allowAuthorized "0"`** (default): requests with auth headers/cookies aren't cached. Turn it on only with permission-sensitive caching in place.
- **`/enableTTL "1"`** makes dispatcher honour `Cache-Control: max-age`/`Expires` from publish — use it for API responses and content that should expire rather than be invalidated.
- **Headers**: `Cache-Control` drives browsers and CDN; `Surrogate-Control` targets the CDN only. Set them in the vhost per path pattern (short TTL for HTML, long+immutable for fingerprinted clientlibs). Cache and serve only the headers you list in `/headers`.

## 4. Invalidation flow

| | AEM 6.5 | Cloud Service |
|---|---|---|
| Dispatcher | Flush replication agent(s) on publish call `/dispatcher/invalidate.cache` | Invalidated automatically on publish/unpublish via content distribution |
| CDN | Your purge integration or short TTLs | Adobe CDN honours TTL headers; keep HTML TTLs short rather than rely on purges |
| Common bug | Flush agent on author only, or pointing at the wrong dispatcher | Stale header/footer XF because statfileslevel isolates it from the pages that include it |

Content included into many pages (XFs, navigation, CF-driven lists) needs a deliberate strategy: shared statfile level, a TTL, or client-side/SDI inclusion.

## 5. Rewrites, redirects, vanity URLs

- **Short URLs**: map `/content/mysite/en/...` ↔ `/en/...` with mod_rewrite `[PT]` rules plus Sling mappings/resource resolver config so links are rewritten on output.
- **Redirects**: small, stable sets in `rewrite.rules` (`RewriteRule ^/old$ /new [R=301,L]`); large marketer-owned lists in a `RewriteMap`. Cloud Service also supports redirect maps maintained outside the code pipeline and CDN-level redirects — confirm the current mechanism in the product docs before designing around it.
- **Vanity URLs** (`sling:vanityPath`): enable the dispatcher `/vanity_urls` feature and allow its endpoint in filters, or rewrite them at Apache; don't let every unknown URL fall through to publish.
- Keep redirect chains to one hop; always `L` flag; test with `curl -I`.

## 6. Headers, CORS, security

- Security headers in the vhost: `Strict-Transport-Security`, `X-Content-Type-Options: nosniff`, `X-Frame-Options`/CSP `frame-ancestors`, `Referrer-Policy`, a CSP owned by whoever owns the tag manager.
- **CORS** for headless: configure the AEM CORS policy OSGi config (`com.adobe.granite.cors.impl.CORSPolicyImpl~<name>`) and pass `Origin` (plus `Access-Control-Request-*`) in `clientheaders.any`; cache per origin or restrict to one origin list so a cached response doesn't carry the wrong `Access-Control-Allow-Origin`.
- Pass only needed client headers; never forward cookies to cacheable paths unless required.

## 7. CDN (Cloud Service) and BYO CDN

- The Adobe-managed CDN (Fastly-based) sits in front of dispatcher. CDN rules live in `config/cdn.yaml` deployed through a Cloud Manager **config pipeline**: traffic filter rules (rate limits, geo/IP blocks), WAF rules (licence-dependent), request/response transformations, origin selectors and redirects. Check current syntax and entitlements in the docs rather than copying old examples.
- Start WAF and rate-limit rules in log/alert mode, tune, then block.
- **BYO CDN**: point it at the Adobe CDN (not around it), send the required host/secret headers per the docs, avoid double caching with conflicting TTLs, and ensure purges or short TTLs on both layers.

## 8. Debugging

- Local: send `X-Dispatcher-Info: true` to the SDK container; the response explains cache/filter decisions. Check `dispatcher.log` with trace level (`DISP_LOG_LEVEL=trace1`).
- Cloud: CDN response headers (`x-cache: HIT/MISS`, `Age`), dispatcher and CDN logs from Cloud Manager; calculate hit ratio per path pattern.
- `404` from dispatcher with a healthy publish = filter deny; `200` but always MISS = auth header/cookie, query string, `Cache-Control: private`, or `/rules` deny.

## Deliverable format

For a config review, return a table per concern, then the corrected snippets:

| URL / pattern | Expected | Actual (filter · cache · TTL) | Risk | Fix |
|---|---|---|---|---|
| `/content/mysite/en.model.json` | 200, cached 5 min | allowed, never cached (query param) | Low hit ratio | Ignore `utm_*` in `/ignoreUrlParams` |
| `/content/mysite/en.infinity.json` | 404 | 200 | Content structure leak | Deny `infinity` selector |

Close with: validator output, the negative test URL list, and expected cache hit ratio impact.

## Anti-patterns to reject

- Allow-all filters with a few denies; `/url` globs instead of structured matchers.
- Editing immutable `default_*` files or `dispatcher.any`; skipping `validate.sh`.
- `/statfileslevel "0"` on multi-site estates, or so high that shared fragments go stale.
- Caching every query-string variant or never caching any; tracking params not ignored.
- `.json`, `.infinity.json`, Query Builder, `/crx` or `/system/console` reachable on publish.
- `/allowAuthorized "1"` without permission-sensitive caching.
- Long CDN TTLs on HTML with no purge strategy; redirect chains and 302s for permanent moves.
- `Access-Control-Allow-Origin: *` on authenticated or cached per-origin responses.
