zuplo 6.74.14 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/articles/api-key-administration.mdx +2 -2
- package/docs/articles/api-key-authentication.mdx +6 -3
- package/docs/articles/api-key-buckets.mdx +19 -5
- package/docs/articles/api-key-consumer-bucket-portal-ui.mdx +20 -15
- package/docs/articles/api-key-self-serve-integration.mdx +2 -2
- package/docs/articles/monetization/api-access.mdx +1 -1
- package/docs/articles/monetization/going-to-production.mdx +1 -1
- package/docs/cli/test.mdx +20 -0
- package/docs/policies/_index.md +3 -1
- package/docs/policies/ai-gateway-metering-v2-inbound/doc.md +20 -0
- package/docs/policies/ai-gateway-metering-v2-inbound/schema.json +1 -1
- package/docs/policies/cdn-cache-control-outbound/doc.md +405 -0
- package/docs/policies/cdn-cache-control-outbound/intro.md +26 -0
- package/docs/policies/cdn-cache-control-outbound/schema.json +275 -0
- package/docs/policies/ip-address-restriction-inbound/doc.md +234 -0
- package/docs/policies/ip-address-restriction-inbound/intro.md +17 -0
- package/docs/policies/ip-address-restriction-inbound/schema.json +97 -0
- package/package.json +6 -6
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
The CDN Cache Control policy sets the response headers a CDN in front of your
|
|
2
|
+
gateway reads to decide how long it may cache a response and which purge tags
|
|
3
|
+
that response belongs to. It keeps that policy separate from the `Cache-Control`
|
|
4
|
+
you send to the browser.
|
|
5
|
+
|
|
6
|
+
## Why the two are separate
|
|
7
|
+
|
|
8
|
+
A single `Cache-Control` header has to serve two audiences with different needs.
|
|
9
|
+
If you write `Cache-Control: public, max-age=1800` so your CDN holds a response
|
|
10
|
+
for 30 minutes, you have also told every browser and mobile app to hold it for
|
|
11
|
+
30 minutes. When the underlying data changes you can purge the edge in seconds —
|
|
12
|
+
but no purge reaches those clients, and they keep serving stale data until the
|
|
13
|
+
TTL runs out.
|
|
14
|
+
|
|
15
|
+
This policy writes the edge TTL into a header only the CDN reads, so the two can
|
|
16
|
+
differ:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Cache-Control: public, max-age=60
|
|
20
|
+
Edge-Control: cache-maxage=30m
|
|
21
|
+
Edge-Cache-Tag: catalog,cities
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The CDN absorbs the traffic for half an hour, clients re-check every minute, and
|
|
25
|
+
a purge by the `cities` tag takes effect immediately for everyone.
|
|
26
|
+
|
|
27
|
+
## Choosing the CDN
|
|
28
|
+
|
|
29
|
+
`cdn` is required and has no default. Each CDN reads a different header, so a
|
|
30
|
+
default would silently produce a well-formed response with no edge caching for
|
|
31
|
+
anyone using a different one.
|
|
32
|
+
|
|
33
|
+
| `cdn` | Edge TTL header | Purge tag header | Tag separator |
|
|
34
|
+
| ------------ | ------------------------------ | ---------------- | ------------- |
|
|
35
|
+
| `akamai` | `Edge-Control` | `Edge-Cache-Tag` | comma |
|
|
36
|
+
| `fastly` | `Surrogate-Control` | `Surrogate-Key` | space |
|
|
37
|
+
| `cloudflare` | `Cloudflare-CDN-Cache-Control` | `Cache-Tag` | comma |
|
|
38
|
+
| `cloudfront` | `Cache-Control: s-maxage` | none | — |
|
|
39
|
+
| `generic` | `CDN-Cache-Control` (RFC 9213) | none | — |
|
|
40
|
+
|
|
41
|
+
Use `generic` for a CDN that implements RFC 9213 targeted cache control but is
|
|
42
|
+
not listed above.
|
|
43
|
+
|
|
44
|
+
## Example Configuration
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"export": "CdnCacheControlOutboundPolicy",
|
|
49
|
+
"module": "$import(@zuplo/runtime)",
|
|
50
|
+
"options": {
|
|
51
|
+
"cdn": "akamai",
|
|
52
|
+
"edge": {
|
|
53
|
+
"maxAge": 1800
|
|
54
|
+
},
|
|
55
|
+
"client": {
|
|
56
|
+
"visibility": "public",
|
|
57
|
+
"maxAge": 60
|
|
58
|
+
},
|
|
59
|
+
"tags": ["catalog", "cities"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
On Fastly the same options emit space-separated surrogate keys, which is the
|
|
65
|
+
format Fastly's purge API expects:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
Cache-Control: public, max-age=60
|
|
69
|
+
Surrogate-Control: max-age=1800
|
|
70
|
+
Surrogate-Key: catalog cities
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## CDN-only caching
|
|
74
|
+
|
|
75
|
+
The most valuable configuration is one a single `Cache-Control` cannot express:
|
|
76
|
+
the response is identical for every caller but requires an `Authorization`
|
|
77
|
+
header, so you want the CDN to absorb the load while no browser stores it.
|
|
78
|
+
|
|
79
|
+
```json
|
|
80
|
+
{
|
|
81
|
+
"export": "CdnCacheControlOutboundPolicy",
|
|
82
|
+
"module": "$import(@zuplo/runtime)",
|
|
83
|
+
"options": {
|
|
84
|
+
"cdn": "cloudflare",
|
|
85
|
+
"edge": { "maxAge": 300 },
|
|
86
|
+
"client": { "visibility": "private", "maxAge": 0 }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
Cache-Control: private, max-age=0
|
|
93
|
+
Cloudflare-CDN-Cache-Control: max-age=300
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
This works because a CDN that honors a targeted cache header ignores
|
|
97
|
+
`Cache-Control` entirely for its own decision — it never sees the `private`.
|
|
98
|
+
|
|
99
|
+
## edge
|
|
100
|
+
|
|
101
|
+
How long the CDN may serve the response.
|
|
102
|
+
|
|
103
|
+
- `maxAge` — seconds the CDN may serve the response without revalidating.
|
|
104
|
+
- `staleWhileRevalidate` — seconds the CDN may keep serving a stale response
|
|
105
|
+
while it revalidates in the background.
|
|
106
|
+
- `staleIfError` — seconds the CDN may serve a stale response when the gateway
|
|
107
|
+
returns an error. This is often the highest-value directive for an API: it
|
|
108
|
+
keeps reads working through a backend outage.
|
|
109
|
+
|
|
110
|
+
Akamai's `Edge-Control` header cannot express the two `stale-*` windows. If you
|
|
111
|
+
need them on Akamai, either configure stale serving in Property Manager or set
|
|
112
|
+
`strategy` to `s-maxage`. The policy rejects the combination rather than
|
|
113
|
+
dropping the directives silently.
|
|
114
|
+
|
|
115
|
+
## client
|
|
116
|
+
|
|
117
|
+
The `Cache-Control` sent to the browser or app.
|
|
118
|
+
|
|
119
|
+
- `maxAge` — seconds the client may reuse the response.
|
|
120
|
+
- `visibility` — `public`, `private`, or `no-store`.
|
|
121
|
+
- `noCache` — adds `no-cache`, so the client revalidates before reusing the
|
|
122
|
+
response. Distinct from `visibility: no-store`, which forbids storing it at
|
|
123
|
+
all.
|
|
124
|
+
- `mustRevalidate` — adds `must-revalidate`, so the client may not serve the
|
|
125
|
+
response once stale.
|
|
126
|
+
- `mode` — how to combine this with a `Cache-Control` the upstream already sent.
|
|
127
|
+
|
|
128
|
+
### client.mode
|
|
129
|
+
|
|
130
|
+
| Mode | Behavior |
|
|
131
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
132
|
+
| `strictest` | **Default.** Emits the more conservative of the two — the lower `max-age`, and the union of restrictive directives. |
|
|
133
|
+
| `replace` | Overwrites whatever the upstream sent. |
|
|
134
|
+
| `preserve` | Leaves the upstream `Cache-Control` untouched and only adds the CDN headers. |
|
|
135
|
+
|
|
136
|
+
`strictest` is the default because loosening a cache directive is a data
|
|
137
|
+
disclosure risk, not only a performance one. If your application returns
|
|
138
|
+
`Cache-Control: private` for a per-user response and the policy widened that to
|
|
139
|
+
`public`, a shared cache could serve one user's data to the next. Use `replace`
|
|
140
|
+
when you intend the gateway to be the authority on client caching.
|
|
141
|
+
|
|
142
|
+
## strategy
|
|
143
|
+
|
|
144
|
+
How the edge TTL reaches the CDN.
|
|
145
|
+
|
|
146
|
+
- `targeted` (default) — the CDN's own header. Required for CDN-only caching.
|
|
147
|
+
- `s-maxage` — folds the edge TTL into `Cache-Control` as `s-maxage`, for a
|
|
148
|
+
property that honors origin `Cache-Control` but not the targeted header.
|
|
149
|
+
|
|
150
|
+
**On CloudFront the default is `s-maxage`**, since it reads no targeted header
|
|
151
|
+
and there is nothing to choose between. You only need to set `strategy`
|
|
152
|
+
explicitly when you want `s-maxage` on a CDN that _does_ read a targeted header
|
|
153
|
+
— for example an Akamai property that honors origin `Cache-Control` but has not
|
|
154
|
+
enabled `Edge-Control`. Setting `strategy: "targeted"` on CloudFront is a
|
|
155
|
+
build-time error, because that is a contradiction rather than an omission.
|
|
156
|
+
|
|
157
|
+
Either way, the policy **removes any targeted header — or purge-tag header —
|
|
158
|
+
your backend already set that the configured CDN reads**, unless the policy is
|
|
159
|
+
writing that header itself. A targeted header outranks `Cache-Control`, so an
|
|
160
|
+
upstream `Surrogate-Control: max-age=99999` reaching Fastly would silently beat
|
|
161
|
+
the TTL configured here — particularly under `s-maxage`, where the policy's own
|
|
162
|
+
TTL is in `Cache-Control`. Only the configured vendor's headers are touched: on
|
|
163
|
+
Cloudflare that means both `Cloudflare-CDN-Cache-Control` and
|
|
164
|
+
`CDN-Cache-Control`, while on CloudFront (which reads neither) nothing is
|
|
165
|
+
removed.
|
|
166
|
+
|
|
167
|
+
That includes the tag header when you configure no `tags`: a backend-set
|
|
168
|
+
`Surrogate-Key` was never checked against the vendor's limits, so it is cleared
|
|
169
|
+
rather than passed through unvalidated.
|
|
170
|
+
|
|
171
|
+
If your backend is the one that should own the edge policy, **do not apply this
|
|
172
|
+
policy to that route.** There is no configuration that lets a backend-set
|
|
173
|
+
targeted header through: `edge` is required unless `cacheConfig` supplies it at
|
|
174
|
+
runtime, and the vendor's headers are stripped on every path — including when a
|
|
175
|
+
response is opted out with `cache: false`, which removes them and writes nothing
|
|
176
|
+
in their place. `client.mode: "preserve"` preserves the _client_
|
|
177
|
+
`Cache-Control` only; it has no bearing on the edge headers.
|
|
178
|
+
|
|
179
|
+
## tags
|
|
180
|
+
|
|
181
|
+
Purge tags, rendered in the target CDN's format. Tags let you invalidate a slice
|
|
182
|
+
of the edge cache — every response tagged `cities` after a city sync — without
|
|
183
|
+
waiting for the TTL or purging everything.
|
|
184
|
+
|
|
185
|
+
Tags are validated against the CDN's limits before they are emitted:
|
|
186
|
+
|
|
187
|
+
| CDN | Max tag length | Max tags | Max header size |
|
|
188
|
+
| ---------- | -------------- | -------- | --------------- |
|
|
189
|
+
| Akamai | 128 chars | 128 | 8192 bytes |
|
|
190
|
+
| Fastly | 1024 bytes | — | 16384 bytes |
|
|
191
|
+
| Cloudflare | 1024 chars | — | 16384 bytes |
|
|
192
|
+
|
|
193
|
+
This validation matters because the failure mode is silent. Fastly ignores the
|
|
194
|
+
key it is parsing _and every key after it_ once a limit is hit, so an over-long
|
|
195
|
+
tag makes later tags quietly unpurgeable. Akamai additionally rejects spaces,
|
|
196
|
+
commas, colons and brackets in tag values.
|
|
197
|
+
|
|
198
|
+
A tag that violates a limit is dropped and logged as a warning rather than
|
|
199
|
+
failing the response — a 200 should not become a 500 over a purge tag — except
|
|
200
|
+
in static configuration, where it is a configuration error you can fix before
|
|
201
|
+
deploying.
|
|
202
|
+
|
|
203
|
+
CloudFront has no purge-by-tag support; use path invalidation instead.
|
|
204
|
+
|
|
205
|
+
## respectUpstream
|
|
206
|
+
|
|
207
|
+
When `true` (the default), an upstream `Cache-Control` of `no-store`, `no-cache`
|
|
208
|
+
or `private`, or a `Set-Cookie` on the response, suppresses every edge header
|
|
209
|
+
and purge tag. Your application keeps the ability to mark a response
|
|
210
|
+
uncacheable, and that keeps working even if the policy is applied broadly.
|
|
211
|
+
|
|
212
|
+
Set it to `false` to override the upstream. On Akamai this emits
|
|
213
|
+
`Edge-Control: !no-store`, the vendor's own directive for caching at the edge
|
|
214
|
+
despite a `no-store`.
|
|
215
|
+
|
|
216
|
+
## vary
|
|
217
|
+
|
|
218
|
+
Request headers that vary the response, emitted as `Vary`. This is **off by
|
|
219
|
+
default** for a reason specific to Akamai: Akamai skips caching any response
|
|
220
|
+
carrying a `Vary` header until Cache ID Modification is configured in Property
|
|
221
|
+
Manager. Turning this on can therefore disable edge caching entirely while the
|
|
222
|
+
response still looks correct. The policy logs a warning when `vary` is set
|
|
223
|
+
together with `cdn: akamai`.
|
|
224
|
+
|
|
225
|
+
The names you configure are **unioned** with any `Vary` the upstream already
|
|
226
|
+
sent, deduplicated case-insensitively; `Vary: *` from either side absorbs the
|
|
227
|
+
rest. Unlike the edge headers, `Vary` is still emitted when an upstream veto or
|
|
228
|
+
`cache: false` suppresses edge caching — a `private, max-age=300` response is
|
|
229
|
+
uncacheable at the edge but still cached by the browser, and it needs the variant
|
|
230
|
+
key just as much. The policy never narrows an upstream `Vary`, because doing so silently is
|
|
231
|
+
unsafe: if the origin sent `Vary: Authorization` and you configure
|
|
232
|
+
`vary: ["accept-language"]`, replacing it would stop a shared cache keying on
|
|
233
|
+
`Authorization` and let it serve one user's authorized response to another. To
|
|
234
|
+
deliberately drop a wasteful upstream `Vary` — `Vary: User-Agent` shreds hit
|
|
235
|
+
rates — remove it in a separate outbound policy before this one runs.
|
|
236
|
+
|
|
237
|
+
If your response genuinely varies by a request header, configure the cache key
|
|
238
|
+
at the CDN — Cache ID Modification on Akamai, Cache Keys on Cloudflare — rather
|
|
239
|
+
than relying on `Vary`.
|
|
240
|
+
|
|
241
|
+
## cacheConfig
|
|
242
|
+
|
|
243
|
+
For rules that cannot be expressed statically, a function can return the cache
|
|
244
|
+
configuration per response.
|
|
245
|
+
|
|
246
|
+
The static options stay the enforced default: **a function that returns nothing
|
|
247
|
+
falls back to them.** So you configure the policy once for the route, and the
|
|
248
|
+
function only has to handle the exceptions.
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
export default function cdnCache(response: Response) {
|
|
252
|
+
// An empty result set is usually a transient upstream problem.
|
|
253
|
+
if (response.headers.get("x-result-count") === "0") {
|
|
254
|
+
return { cache: false };
|
|
255
|
+
}
|
|
256
|
+
// Everything else: fall back to the configured edge/client/tags.
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
When the function _does_ return a config, that config **replaces** the static
|
|
262
|
+
options rather than being merged into them field by field — so a returned
|
|
263
|
+
`{ edge: { maxAge: 60 } }` drops a statically configured `staleIfError` and the
|
|
264
|
+
static `tags`. Return the complete policy for the responses you override.
|
|
265
|
+
|
|
266
|
+
### Reading the response body
|
|
267
|
+
|
|
268
|
+
The function receives the live response, not a copy. **Do not consume the body**
|
|
269
|
+
— the policy still has to forward it, and reading it first makes that
|
|
270
|
+
impossible. If you need to inspect the payload, clone it:
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const body = await response.clone().json();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Cloning is left to you rather than done for every response: `clone()` tees the
|
|
277
|
+
stream, and a branch nobody reads keeps the whole body in memory. Most rules can
|
|
278
|
+
avoid the body entirely — a response header such as `x-result-count` is cheaper
|
|
279
|
+
than parsing JSON on every cacheable response.
|
|
280
|
+
|
|
281
|
+
If the body is consumed without cloning, the policy fails the request with a
|
|
282
|
+
message naming the cause, rather than letting the response go out truncated.
|
|
283
|
+
|
|
284
|
+
The three return values:
|
|
285
|
+
|
|
286
|
+
| Return | Effect |
|
|
287
|
+
| ------------------ | ----------------------------------------------------------------------------------- |
|
|
288
|
+
| `undefined`/`null` | Use the static options unchanged. |
|
|
289
|
+
| a config object | Use it in place of the static options. |
|
|
290
|
+
| `{ cache: false }` | Emit no edge headers or tags, **and** send `Cache-Control: no-store` to the client — except under `client.mode: "preserve"`, which is a trap; see below. |
|
|
291
|
+
|
|
292
|
+
`{ cache: false }` is a positive statement that the response must not be cached
|
|
293
|
+
anywhere, so it disables client caching as well as edge caching.
|
|
294
|
+
|
|
295
|
+
**Under `client.mode: "preserve"` it does not, and this is a sharp edge.** The
|
|
296
|
+
policy suppresses its own edge headers but leaves the upstream `Cache-Control`
|
|
297
|
+
alone — and that header is exactly what a CDN falls back to once the targeted
|
|
298
|
+
header is gone. An upstream `Cache-Control: public, max-age=300` therefore keeps
|
|
299
|
+
the response cacheable at the edge for five minutes despite the `cache: false`.
|
|
300
|
+
On CloudFront, which reads no targeted header at all, `cache: false` under
|
|
301
|
+
`preserve` changes nothing whatsoever.
|
|
302
|
+
|
|
303
|
+
To make the edge skip a response while clients still cache it normally, return
|
|
304
|
+
`{ edge: { maxAge: 0 } }` with `client.mode: "preserve"`. That states a zero
|
|
305
|
+
edge TTL in the CDN's own dialect — `Surrogate-Control: max-age=0` on Fastly,
|
|
306
|
+
`s-maxage=0` appended to the client's own directives on CloudFront — rather than
|
|
307
|
+
relying on the absence of a header to mean "do not cache".
|
|
308
|
+
|
|
309
|
+
```json
|
|
310
|
+
{
|
|
311
|
+
"export": "CdnCacheControlOutboundPolicy",
|
|
312
|
+
"module": "$import(@zuplo/runtime)",
|
|
313
|
+
"options": {
|
|
314
|
+
"cdn": "fastly",
|
|
315
|
+
"cacheConfig": {
|
|
316
|
+
"module": "$import(./modules/cdn-cache)",
|
|
317
|
+
"export": "default"
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import {
|
|
325
|
+
CdnCacheControlConfig,
|
|
326
|
+
ZuploContext,
|
|
327
|
+
ZuploRequest,
|
|
328
|
+
} from "@zuplo/runtime";
|
|
329
|
+
|
|
330
|
+
export default function cdnCache(
|
|
331
|
+
response: Response,
|
|
332
|
+
request: ZuploRequest,
|
|
333
|
+
context: ZuploContext
|
|
334
|
+
): CdnCacheControlConfig {
|
|
335
|
+
// An empty result set is usually a transient upstream problem — don't cache it.
|
|
336
|
+
if (response.headers.get("x-result-count") === "0") {
|
|
337
|
+
return { cache: false };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const cityId = new URL(request.url).searchParams.get("cityId");
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
edge: { maxAge: 1800, staleIfError: 86400 },
|
|
344
|
+
client: { maxAge: 60, visibility: "public" },
|
|
345
|
+
tags: ["cities", `city-${cityId}`],
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
The function returns intent, not headers: the policy renders it into whichever
|
|
351
|
+
dialect `cdn` names, so switching CDNs is a one-line configuration change rather
|
|
352
|
+
than a rewrite. Per-entity tags like `city-42` are the main thing that cannot be
|
|
353
|
+
expressed statically, and they are what makes fine-grained purging possible.
|
|
354
|
+
|
|
355
|
+
Note that `{ cache: false }` and `undefined` are **not** the same:
|
|
356
|
+
`cache: false` suppresses caching for that response, while `undefined` falls
|
|
357
|
+
back to the static options.
|
|
358
|
+
|
|
359
|
+
## Using this with the Caching policy
|
|
360
|
+
|
|
361
|
+
This policy sets headers for a CDN _outside_ Zuplo. The
|
|
362
|
+
[Caching](https://zuplo.com/docs/policies/caching-inbound) policy is a separate,
|
|
363
|
+
gateway-side response cache.
|
|
364
|
+
|
|
365
|
+
**Do not use the two on the same route.** They are alternative places to cache,
|
|
366
|
+
not layers that stack, and combining them gives you neither policy reliably.
|
|
367
|
+
|
|
368
|
+
The Caching policy serves a hit from an _inbound_ policy, which short-circuits
|
|
369
|
+
the pipeline before outbound policies run — so this policy does not execute on a
|
|
370
|
+
gateway cache hit. The CDN instead receives the copy the Caching policy stored,
|
|
371
|
+
which it sanitizes on the way in: several CDN headers are removed and
|
|
372
|
+
`Cache-Control` is overwritten with its own `s-maxage` (60s by default). Cache
|
|
373
|
+
misses behave correctly, so the problem is invisible until traffic warms the
|
|
374
|
+
cache.
|
|
375
|
+
|
|
376
|
+
Pick one:
|
|
377
|
+
|
|
378
|
+
- **Cache at the CDN** — use this policy alone. Purges are global and fast, and
|
|
379
|
+
the `edge`/`client` split works as configured.
|
|
380
|
+
- **Cache at the gateway** — use the Caching policy alone, and let it own the
|
|
381
|
+
`Cache-Control` it sends.
|
|
382
|
+
|
|
383
|
+
## Best Practices
|
|
384
|
+
|
|
385
|
+
1. **Make the client TTL much shorter than the edge TTL.** The edge is what you
|
|
386
|
+
can purge; clients are not. A short client `max-age` with a long
|
|
387
|
+
`edge.maxAge` gives you both offload and control.
|
|
388
|
+
2. **Set `staleIfError`** on read endpoints where CDN support allows it. Serving
|
|
389
|
+
slightly stale data through a backend outage is almost always better than
|
|
390
|
+
serving an error.
|
|
391
|
+
3. **Tag by entity, not just by route.** `city-42` lets you purge one city;
|
|
392
|
+
`cities` alone forces you to purge them all.
|
|
393
|
+
4. **Leave `respectUpstream` on** so the application retains the final say.
|
|
394
|
+
5. **Verify at the edge, not just at the gateway.** `curl -I` against your Zuplo
|
|
395
|
+
URL shows the headers; whether the CDN honors them depends on its
|
|
396
|
+
configuration (Akamai's "Honor origin Cache-Control", Cloudflare's Origin
|
|
397
|
+
Cache Control, a Cache Rule that makes the path cacheable at all).
|
|
398
|
+
|
|
399
|
+
## Limitations
|
|
400
|
+
|
|
401
|
+
- Purge tags are emitted, not purged. Invalidation is an API call to your CDN
|
|
402
|
+
from your own tooling.
|
|
403
|
+
- Edge cache-key configuration is not expressible in response headers and
|
|
404
|
+
remains CDN configuration.
|
|
405
|
+
- CloudFront supports neither a targeted cache header nor purge tags.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
The CDN Cache Control policy sets the response headers that tell a CDN in front
|
|
2
|
+
of your gateway how long it may cache a response, and which purge tags that
|
|
3
|
+
response belongs to — separately from what you tell the browser.
|
|
4
|
+
|
|
5
|
+
With this policy, you'll benefit from:
|
|
6
|
+
|
|
7
|
+
- **Two audiences, two policies**: let the CDN hold a response for 30 minutes
|
|
8
|
+
while browsers hold it for 60 seconds, so a purge at the edge actually takes
|
|
9
|
+
effect
|
|
10
|
+
- **CDN-only caching**: cache an authenticated response at the edge without any
|
|
11
|
+
browser or intermediary storing it
|
|
12
|
+
- **No dialect to learn**: Akamai's `Edge-Control` and `Edge-Cache-Tag`,
|
|
13
|
+
Fastly's `Surrogate-Control` and space-separated `Surrogate-Key`, Cloudflare's
|
|
14
|
+
`Cache-Tag` — you configure intent and the policy writes the right headers
|
|
15
|
+
- **Purge tags with guard rails**: tags are validated against each CDN's length,
|
|
16
|
+
count and character limits, because an over-long tag header is silently
|
|
17
|
+
truncated at the edge rather than rejected
|
|
18
|
+
- **Safe by default**: an upstream `no-store`, `private` or `Set-Cookie` keeps
|
|
19
|
+
its veto, and the default merge mode never loosens a policy your application
|
|
20
|
+
deliberately tightened
|
|
21
|
+
- **Dynamic configuration**: a function can return per-response TTLs and
|
|
22
|
+
per-entity purge tags for anything that can't be expressed statically
|
|
23
|
+
|
|
24
|
+
Set `cdn` to the CDN actually in front of your gateway. It has no default,
|
|
25
|
+
because every CDN reads a different header and a wrong guess produces a
|
|
26
|
+
well-formed response with no edge caching at all.
|