Header and cookie filters
GraphPilot lets you drop specific headers and cookies by name at four points in a request’s life. Each point answers a different question, and they do not share a list.
The four filter points
Section titled “The four filter points”| Section | Applies to | When |
|---|---|---|
[origin., [origin. | The request, on its way to your origin | Just before the request is forwarded, after the cache decision has already been made from the request as the client sent it |
[response., [response. | The response, on its way to your client | On delivery, which includes a cache hit served from an entry written before this configuration existed |
[cache. | Which response headers are written into a cache entry | When the entry is stored |
[cache. | Which cookie names the cache treats as carrying no session | Read by both the Set-Cookie storage rule and the credentialed-request rule, and by [cache.’s own handling of Set-Cookie |
[origin]headers = { hide = ["x-internal-debug"] }cookies = { hide = ["internal_ab_test"] }
[response]headers = { hide = ["x-origin-latency-ms"] }cookies = { hide = ["__internal_session"] }
[cache.headers]exclude = ["x-request-id"]allow = ["www-authenticate"]
[cache.cookies]allow = ["GAESA", "my_custom_lb_cookie"][origin.headers] and [origin.cookies] have no cache semantics. The cache lookup, the store
decision and policy expressions have already run against the request as the client sent it, so a
name dropped here never reaches your origin but was never withheld from the cache decision.
[cache.headers] is two independent lists. exclude names headers that are not written to the
entry at all: the client that caused the miss still gets them, a later hit does not. allow
names headers that no longer refuse the store, out of the fixed list GraphPilot would otherwise
refuse a response for: WWW-Authenticate, Proxy-Authenticate, Authentication-Info,
Proxy-Authentication-Info, Clear-Site-Data, Set-Login. An allowed header is also never
written to the entry, since allowing it says it carries nothing worth replaying.
Set-Cookie and Set-Cookie2 are refused in [cache.headers].allow too, even though they are on
the fixed refusal list: each cookie a Set-Cookie sets is checked against [cache.cookies].allow
first, per name, so allowing the header could never have affected them. Use
[cache.cookies].allow to allow a specific cookie.
hide versus only
Section titled “hide versus only”[origin.headers], [origin.cookies], [response.headers] and [response.cookies] share one
shape:
| Key | Names | Everything else |
|---|---|---|
hide | Dropped | Survives |
only | Survives | Dropped, except the protected names below |
Setting both hide and only on the same section is refused at deploy time, because “hide a,
keep only b” has no unambiguous reading.
Conditional rules for the response
Section titled “Conditional rules for the response”[response.headers] and [response.cookies] also accept rules that apply only to some callers.
Each [[response.headers.rules]] or [[response.cookies.rules]] entry holds a hide or an
only list and an optional when, a policy expression
evaluated as a truth for each request:
[response.headers]hide = ["x-origin-debug"]
[[response.headers.rules]]hide = ["x-debug-trace"]when = "auth:idp.role != 'admin'"Everyone loses x-origin-debug. Only admins see x-debug-trace.
The same shape keeps an origin’s Server-Timing header, which tells anyone who reads it how long
each of your backend calls took, for your own staff:
[[response.headers.rules]]hide = ["server-timing"]when = "!auth:idp.role || auth:idp.role != 'admin'"A filter on delivery applies to a hit as well, so a hit served to a customer carries no
Server-Timing even when the entry was filled by an admin’s request.
For a caller with no role, that when is undecidable, and an undecidable condition reads as
the rule applies, so an anonymous caller loses x-debug-trace too: a filter that cannot tell
whether a condition held must not be the one that reveals. Writing
!auth:idp.role || auth:idp.role != 'admin' says the same thing explicitly.
See a request the expression cannot
decide.
How the lists combine:
- The plain
hideoronlylist applies first, then every rule whosewhenmatched, in the order they are written. A name reaches the caller only if every list that applies keeps it: ahiderule adds to what is dropped, anonlyrule narrows what is kept. - A rule without
whenapplies to every request. - A rule has to name something in
hideoronly, and may not set both. Its names follow the same protected names and matching rules as the plain lists. Each of these mistakes refuses the deployment, and the message names the rule by its position, such asresponse.headers.rules[0]. whenmay readauth:claims: the rules are decided after authentication has run. A request that is refused or passed on before authentication completes is decided without claims, the same as every other policy sees it.
Protected names
Section titled “Protected names”Some names a filter is never allowed to touch, whatever hide or only says:
- GraphPilot’s own
gp-namespace (gp-cache,gp-cache-reason,gp-surrogate-key,gp-origin-retries, and every other header GraphPilot sets itself). To withhold the cache status or surrogate key headers from your clients, useresponse.cache_headersandresponse.surrogate_key_headerinstead. See switching the headers on and off. - A fixed list per direction:
host,content-type,content-encoding,content-lengthandtransfer-encodingfor a request-side filter ([origin.headers]);content-type,content-encoding,cache-control,varyandsurrogate-keyfor a response-side or cache-side filter ([response.headers],[cache.headers]).
The bar for being on these lists is silent corruption. GraphPilot compresses the response toward
the client itself, so a filter that removed Content-Encoding would hand your client compressed
bytes that no longer describe themselves, and nothing would report an error. See compressed
origin responses.
Headers GraphPilot never reads are deliberately not protected, including Age, Date,
Expires, ETag and Last-Modified: hiding one costs exactly what you asked to hide.
Naming one of these in hide or only is a deployment failure, not a line that is quietly
ignored. An only list does not have to enumerate them either: they survive whether or not they
are listed, so allowlisting one header does not cost you your content type.
Cookie names are not checked against this list. A cookie called vary is an ordinary cookie, not
the header of the same name, so [origin.cookies], [response.cookies] and
[cache.cookies].allow accept it.
Matching
Section titled “Matching”Every list matches either as an exact name or, if the entry ends in a trailing *, as a prefix.
BIGipServer* matches BIGipServerpool_web and also matches BIGipServer itself. A *
anywhere except the very end is an ordinary character: there is no general glob and no leading
wildcard, because a match wider than its author expected is the expensive direction to be wrong
in.
Case sensitivity depends on what is being matched, and the two halves disagree on purpose:
| Names | Case |
|---|---|
Header names ([origin., [response., [cache., [cache.) | Case-insensitive, because HTTP treats a header name that way. X-Debug and x-debug are the same header. |
Cookie names ([origin., [response., [cache.) | Case-sensitive, because RFC 6265 treats a cookie name that way. GAESA and gaesa are two different cookies. |
The trailing-* prefix form follows the same rule per list: BIGipServer* in
[cache.cookies].allow matches BIGipServerpool_web and does not match
bigipserverpool_web, while X-Debug* in [response.headers] matches x-debug-info just as
it matches X-Debug-Info.
What cache.cookies.allow actually does
Section titled “What cache.cookies.allow actually does”A name on [cache.cookies].allow has three effects, and only three:
- A
Cookievalue made only of allowed names does not make the request credentialed for the purposes of the authenticated-request rule. - An allowed name inside a
Set-Cookievalue does not by itself refuse storing the response. - An allowed name is never written to the cache entry, so a hit never replays it.
The cookie still reaches your origin and your client untouched; allowing a name only tells GraphPilot that it carries no session.
The default sessionless cookies (cache.cookies.allow)
Section titled “The default sessionless cookies (cache.cookies.allow)”Infrastructure in front of your origin routinely sets its own cookies, none of them carrying any
identity, and each would otherwise disable the cache for the traffic behind it. GraphPilot ships a
default cache.cookies.allow list:
GAESA, GCLB, AWSALB, AWSALBCORS, AWSELB, ARRAffinity, ARRAffinitySameSite, BIGipServer*,__cf_bm, __cflb, incap_ses_*, visid_incap_*Three things worth knowing before you touch this key:
- Setting
cache.cookies.allowreplaces the shipped list, it does not add to it. If you add your own cookie, restate every default you still need alongside it. - Analytics cookies (
_ga,_gid,_fbp) are deliberately not on the list. They identify a visitor, and silently dropping them from a cache entry would change a customer’s analytics without anybody asking for it. - Matching is case-sensitive. See matching.
Example: an A/B test cookie
Section titled “Example: an A/B test cookie”Your client assigns each visitor a bucket in a cookie, ab_bucket=a or ab_bucket=b, and your
origin answers the two buckets differently. Out of the box that cookie works against the cache: a
request that sends it counts as
credentialed, so a response
whose Cache-Control says neither public nor s-maxage is filed per caller or not stored, and a
response carrying Set-Cookie: ab_bucket=... is not stored at all (CACHE_SKIPPED_SET_COOKIE).
Tell GraphPilot the cookie carries no session, and key the cache on the variant:
[cache.cookies]allow = [ # The defaults, restated: setting the key replaces the shipped list. "GAESA", "GCLB", "AWSALB", "AWSALBCORS", "AWSELB", "ARRAffinity", "ARRAffinitySameSite", "BIGipServer*", "__cf_bm", "__cflb", "incap_ses_*", "visid_incap_*", "ab_bucket",]
[[cache.policy.vary]]name = "x-ab-variant-b"value = "in(cookie:ab_bucket, ['b']) || 'false'"pass_to_origin = "plain"The rule derives exactly two values, true for bucket b and false for everything else (bucket
a, no cookie, or a value somebody made up), so each response has two entries however many cookie
values arrive. Your origin receives x-ab-variant-b: true or false and answers from that header.
Assignment is the visitor’s to change: anyone can send ab_bucket=b. That is fine for a test and
never for anything a visitor should not be able to choose; see a value is only as trustworthy as
its source.
The requests to send, what Insights shows and the pitfalls are in the recipe A/B testing with a
cookie.