Skip to content

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.

SectionApplies toWhen
[origin.headers], [origin.cookies]The request, on its way to your originJust before the request is forwarded, after the cache decision has already been made from the request as the client sent it
[response.headers], [response.cookies]The response, on its way to your clientOn delivery, which includes a cache hit served from an entry written before this configuration existed
[cache.headers]Which response headers are written into a cache entryWhen the entry is stored
[cache.cookies]Which cookie names the cache treats as carrying no sessionRead by both the Set-Cookie storage rule and the credentialed-request rule, and by [cache.headers]’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.

[origin.headers], [origin.cookies], [response.headers] and [response.cookies] share one shape:

KeyNamesEverything else
hideDroppedSurvives
onlySurvivesDropped, 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.

[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 hide or only list applies first, then every rule whose when matched, in the order they are written. A name reaches the caller only if every list that applies keeps it: a hide rule adds to what is dropped, an only rule narrows what is kept.
  • A rule without when applies to every request.
  • A rule has to name something in hide or only, 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 as response.headers.rules[0].
  • when may read auth: 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.

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, use response.cache_headers and response.surrogate_key_header instead. See switching the headers on and off.
  • A fixed list per direction: host, content-type, content-encoding, content-length and transfer-encoding for a request-side filter ([origin.headers]); content-type, content-encoding, cache-control, vary and surrogate-key for 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.

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:

NamesCase
Header names ([origin.headers], [response.headers], [cache.headers].exclude, [cache.headers].allow)Case-insensitive, because HTTP treats a header name that way. X-Debug and x-debug are the same header.
Cookie names ([origin.cookies], [response.cookies], [cache.cookies].allow)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.

A name on [cache.cookies].allow has three effects, and only three:

  1. A Cookie value made only of allowed names does not make the request credentialed for the purposes of the authenticated-request rule.
  2. An allowed name inside a Set-Cookie value does not by itself refuse storing the response.
  3. 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.allow replaces 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.

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:

gpilot.toml
[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.