HTTP headers
GraphPilot reserves the gp- prefix for headers it sets itself. The namespace is protected in
both directions: a client cannot send a gp- header (every one is dropped by
prefix), and your origin cannot send one of the headers
GraphPilot sets (each is removed by name). For reading
these headers when something looks wrong, see troubleshooting.
Headers your client receives
Section titled “Headers your client receives”GraphPilot sets ten response headers. They are the only gp- names GraphPilot itself puts on a
response.
| Header | Value | Controlled by | When it appears |
|---|---|---|---|
gp-cache | HIT, MISS, PASS or STALE. See cache statuses. | response. (default on) | Every response that went through the cache, including a PASS. Absent on a request refused before the cache (a 401 from authentication, for example), and on every response the cache would otherwise have looked up while the service runs in lean mode. In lean mode, a request that bypasses the cache still reports PASS with its reason. |
gp- | A stable code: CACHE_PASS_* or CACHE_. See codes. | response. | On a PASS. Absent on a HIT, a STALE, and a MISS (which always means the response was stored). |
gp- | How many times the copy on the cache server that answered has been served. Approximate: the entry can be stored on several servers, each counting on its own, so the value can go down between two reads. For hits per entry, use Insights. | response. | On a HIT or STALE. |
gp-cache-age | Seconds since the entry was written. | response. | On a HIT or STALE. |
gp- | The freshness lifetime (TTL) the entry was stored with, in seconds. | response. | On a HIT or STALE. |
gp- | Seconds of freshness left. | response. | On a HIT or STALE. |
gp- | The number of repeats. See origin retries. | [origin.; not affected by any [response] switch | When the origin request had to be repeated at least once. |
gp- | The keys, space separated, whole keys only: the cache key (ck:) and root field keys (q:, m:) first, entity keys last. | response. (default off), response. (default 8192) | When switched on and the response carries surrogate keys that fit the size limit. |
gp- | How many keys gp- left out. If no key fits at all, this header is sent without gp-. | response. | When switched on and some keys did not fit. |
gp- | The id GraphPilot recorded this request under, shown as the Request ID column in the portal’s Insights and as Entry ID in the request’s Overview tab. Minted per request, never stored with a cache entry. | response. (default off) | When switched on, on every response. |
Other headers GraphPilot writes or removes
Section titled “Other headers GraphPilot writes or removes”| Header | What GraphPilot does |
|---|---|
Cache- | Written for the client when cache. is if_cacheable or always. Otherwise your origin’s header passes through. A response GraphPilot produces itself (a rejection) carries Cache-Control: no-store. |
Allow | Written on a 405 (METHOD_), naming the method the service accepts for that operation. |
Content- | The edge compresses the response for the client according to the client’s Accept-, on a hit exactly as on a miss. |
x-cache, x-cache-hits | Removed. The underlying edge platform (Fastly) adds them to describe GraphPilot’s own fetch from your origin, so they say nothing about the GraphPilot cache and could contradict gp-cache. |
surrogate- | Removed. GraphPilot has read your origin’s Surrogate- header and its keys are part of gp-. See the Surrogate-Key header. |
Reserved names on an origin response
Section titled “Reserved names on an origin response”Your origin cannot set the gp- headers above, or the ones GraphPilot uses between its edge
locations. GraphPilot removes them, by name, from every origin response before anything else
reads it, so an origin cannot make its own
behavior look like GraphPilot’s or get one of them stored in a cache entry. Other gp- names your
origin sends are left alone.
Switching the headers on and off
Section titled “Switching the headers on and off”Four keys under [response] decide
whether and how much a client sees of these headers.
| Key | Default | Controls |
|---|---|---|
response. | true | The whole cache status family: gp-cache, gp-, gp-, gp-cache-age, gp- and gp-. |
response. | false | gp- and gp-. |
response. | 8192 | How long the value of gp- may get. See size limit. |
response. | false | gp-. See request ids. |
| Fact | Detail |
|---|---|
| Independent of each other | Each switch changes only what leaves GraphPilot. |
| The cache is unaffected | A response is stored, served and purged the same way whether a client sees the headers or not. Surrogate keys are still written to the entry, so purging by key works with surrogate_key_header = false. |
| Telemetry is unaffected | Request telemetry records the cache status, the reason code and the keys either way. |
| Applied on delivery | Like [response., the switches also govern a hit served from an entry written before you changed them. |
gp- | Not affected by any switch. |
The only way to withhold a gp- header | [response. cannot hide one: the namespace is protected, and naming one there refuses the deployment. |
Size limit
Section titled “Size limit”gp-surrogate-key stops at response.surrogate_key_header_max_bytes, 8,192 bytes by default.
| Rule | Detail |
|---|---|
| What it measures | The value of gp- alone, not all headers of the response together. The default is half of the 16 KiB Node.js accepts for all headers of a response, so the other headers still fit. |
| Whole keys only | A key is never cut in half. |
| Order | The cache key (ck:), then the root field keys (q:, m:), then type keys, then static keys, keys from your origin’s Surrogate- header and empty-list keys, and entity keys last. When the list is cut, the entity keys go first. |
| Keys left out | Counted in gp-. Without that header, the list is complete. If not even one key fits, gp- is left off and only gp- is sent. |
| One header | The keys are never split across several header lines (RFC 9110, section 5.3). |
| Accepted values | 1 to 128000. The upper bound is Fastly’s limit for a single response header; past it the response would fail with a 503, so a larger value refuses the deployment. |
| The entry is unaffected | The entry is tagged with every key, and a purge by any of them works whether it appeared in the header or not. |
Headers a client can send
Section titled “Headers a client can send”Everything a client sends reaches your origin unless a row below or your
[origin.headers] filter says
otherwise.
| Header | What GraphPilot does with it | Reaches your origin |
|---|---|---|
gp-, gp- | Kept for client profiles: a policy expression reads them as header:gp- and header:gp-. Removed from the request before it is forwarded. | No |
GP-, or the name set in telemetry. | Its value is recorded on the request’s telemetry row and on every log line for the request. Nothing is generated when it is absent. See correlating logs. | Yes, unchanged, under the default name too. When a client sends it more than once, only the first value is forwarded, the one telemetry records. The value is whatever the client sent and is not validated, so treat it as untrusted input even though the default name is in the gp- namespace. To keep it out of that namespace, set telemetry. to a name such as x-. |
Any other gp- header | Dropped on arrival and logged as INGRESS_. | No |
Cache- | Its no-store, no-cache and max-age=0 directives are read under [cache.. | Yes |
Authorization, or the header an [auth. block reads | Verified by that provider; its claims are available as auth:<name>.. A credential makes the request credentialed for caching. A Bearer prefix is stripped before verification. | Yes |
Cookie | A cookie not on cache. makes the request credentialed. | Yes, minus [origin. |
Origin, Access-, Access- | On a cached CORS preflight, part of the entry’s key. See CORS. | Yes |
A header a [[cache. entry names | Removed and logged as INGRESS_; the entry writes its own derived value under that name. | Only as pass_ allows |
Headers dropped on the way in
Section titled “Headers dropped on the way in”Every request header starting with gp- is removed when the request arrives, before any setting
or policy expression reads it, and each one is logged as
INGRESS_RESERVED_HEADER_DROPPED with its name. gp-client-name and gp-client-version are
read first, as the table above says, and removed all the same. The correlation header is the one
exception: it is set aside before the removal and written back afterwards, so it reaches your
origin and is not logged as dropped. A name GraphPilot acts on itself (gp-signature,
gp-timestamp, gp-client-ip, the client names, or any other name GraphPilot uses internally)
is never excepted, even when configured as the correlation header.
The client’s own Cache-Control
Section titled “The client’s own Cache-Control”What the directives a client sends on its own request do (RFC 9111,
section 5.2.1). Each is a switch under
[cache.request_directives]. A hard reload in
a browser sends no-cache.
| Request directive | Key | Default | When honored |
|---|---|---|---|
no-store | no_store | "honor" | The cache is still read, so the request can be answered from an entry another request stored. Only the response to this request is not written: CACHE_. |
no-cache, or max-age=0 | no_cache | "ignore" | The request goes straight to your origin with no lookup, and its response is not written back, the same path a bypass takes: PASS, CACHE_. |
| Default | Why |
|---|---|
no_cache = "ignore" | Any caller can send it. Honored, it lets a client put every one of its requests past the cache and onto your origin. Even when honored, the response is not written back, so it never overwrites the entry other clients read. |
no_store = "honor" | The lookup still happens, so it never causes an origin request that would not have happened anyway; at most one entry is not written. |
A value other than "honor" or "ignore", or an unknown key in the section, is refused when the
configuration is read.
Origin headers GraphPilot reads
Section titled “Origin headers GraphPilot reads”| Header on your origin’s response | What GraphPilot does |
|---|---|
Cache- | The only header that decides a lifetime. Which directives are read, and in which order, is on lifetime from the origin header. stale- is parsed and not acted on. |
Surrogate- | Keys to tag the entry with; on a response to a write, keys to purge. See the Surrogate-Key header. Removed before the response reaches the client. |
Vary | Every request header it names becomes part of the entry’s key. Vary: * is never stored (CACHE_). See your origin’s Vary header. |
Set-Cookie, Set-Cookie2 | A cookie not on cache. refuses storing (CACHE_). |
WWW-, Proxy-, Authentication-, Proxy-, Clear-, Set-Login | Refuse storing (CACHE_), unless named in cache.. |
Content- | A compressed body is decoded first (gzip, deflate, br, zstd). See compression. |
Surrogate- | Ignored. |
Expires | Ignored. |
Age, Date, ETag, Last- | Not consulted. An entry’s age comes from the cache itself. |
The gp- headers GraphPilot sets | Removed. See reserved names. |
Which of these refuse a response whatever its Cache-Control says is on when a response is
cached.
Request headers your origin receives
Section titled “Request headers your origin receives”GraphPilot changes a few headers on the request before forwarding it. Everything else arrives as
the client sent it, minus the forwarding headers a client sent and what
your [origin.headers] and
[origin.cookies] filters remove.
| Header | What your origin receives | Controlled by |
|---|---|---|
Host | The hostname of your service’s origin URL, not the hostname the client called. The same name is sent for TLS (SNI) and checked against the origin’s certificate. | The origin URL. See origin connection. |
gp-timestamp | The Unix time, in seconds, at which GraphPilot signed the request. | Always sent. See request signing. |
gp-signature | v1= followed by the Base64 HMAC-SHA256 signature over the method, path and query, timestamp and body hash. | Always sent. A request GraphPilot cannot sign is not forwarded. |
GP-, or the name set in telemetry. | The value the client sent, unchanged and not validated, and only the first one when it was sent more than once. It is not covered by gp-signature, so treat it as untrusted input even under the default gp- name. Nothing is sent when the client sent none. | telemetry.; an [origin. filter can remove it. |
Accept- | Always identity, replacing what the client sent. GraphPilot reads the body to derive surrogate keys and strip injected fields, and compresses toward the client itself. An origin that compresses anyway is decoded (ORIGIN_) or answered with 502. See codes. | Not configurable. An [origin. only list does not remove it. |
| The name you choose | The GraphPilot request id, the same value as gp-. A client-sent header of the same name is replaced. | origin. (default unset, so nothing is sent). A gp- name refuses the deployment. |
gp-client-ip | The client’s IP address, IPv4 or IPv6, as the edge saw the connection; on a shielded request, the address the first edge location saw, never a GraphPilot address. Not sent when the address is unknown. A client cannot set it, since every gp- header a client sends is dropped on arrival. Like the other headers it is not part of gp-signature, so trust it on a request whose signature verifies. | origin., default false, so nothing is sent. An [origin. only list does not remove it. telemetry. does not change it either, but it decides whether a [telemetry. rule may record it: only under client_ip = "full". |
Every X- header, Forwarded, X-Real-IP and the other forwarding headers | Nothing by default: the values a client sent are removed. With "pass", the values exactly as the client sent them. GraphPilot never writes these headers itself. | origin., default "strip". |
A [[cache. header | Nothing by default. With pass_to_origin = "anonymized", a keyed digest (v1= plus base64url); with "plain", the derived value. | pass_ |
Content-Type, Content- | Content- is restated for the body actually sent. A GET whose operation GraphPilot rewrote (a persisted query hash resolved to its document) is sent as POST, or QUERY under method_rewrite = "post_to_query", with a JSON body and Content-Type: application/json. | origin. |
Nothing else in the gp- namespace reaches your origin: apart from the correlation header above,
GraphPilot strips the whole namespace before it signs, then adds gp-timestamp and gp-signature,
and gp-client-ip only when you turn on origin.forward_client_ip. A fresh cache hit is answered at the
edge, so no request, and no request id, reaches your origin for it.
Forwarding headers
Section titled “Forwarding headers”Forwarding headers tell a server where a request came from and which URL it asked for. A proxy in front of a server normally writes them. GraphPilot writes none of them, so on a request that reaches the edge every value in them is one the client chose.
What counts as a forwarding header
Section titled “What counts as a forwarding header”- Every header whose name starts with
X-Forwarded-, in any case:X-Forwarded-For,X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-Port,X-Forwarded-Prefix,X-Forwarded-Client-Cert, and any other name with that prefix.X-Forwardedwithout the trailing dash is an ordinary header. - These ten names, matched whole and in any case:
Forwarded,X-Real-IP,X-Client-IP,Client-IP,True-Client-IP,CF-Connecting-IP,Fastly-Client-IP,X-Original-URL,X-Rewrite-URL,X-Original-Host.
Why they are removed
Section titled “Why they are removed”An origin that reads them gets one of two problems:
- A spoofed address. A framework configured to trust a proxy (Express
trust proxy, Rails, Symfony, nginxreal_ip) takes the client’s address fromX-Forwarded-FororForwarded. A rate limit or an IP allowlist built on it uses an address the client picked. - A poisoned cache entry. An origin that builds absolute URLs from
X-Forwarded-HostorX-Forwarded-Protoanswers with the client’s URLs. These headers are not part of the cache key, so that answer is stored and served to every later caller.
When they are removed
Section titled “When they are removed”With origin.forwarded_headers at its default, "strip",
every forwarding header is removed right before the request is sent to your origin, after your
[origin.headers] filter has run. Only the request to your origin changes:
- GraphPilot itself reads the request as the client sent it. A policy expression such as
header:x-forwarded-forstill resolves, a[[cache.policy.vary]]entry can still derive a value from one, andtelemetry.headers.requestrecords the headers the client sent.telemetry.headers.origin_requestrecords what your origin received, so the forwarding headers are absent there. - On a shielded service, the edge location passes the headers on to the shield, and the shield removes them before it calls your origin.
- A cache hit never reaches your origin, so nothing is removed there.
Removing further headers
Section titled “Removing further headers”The strip covers forwarding headers only. To keep other headers from your origin, list them in
[origin.headers]. A trailing *
matches a prefix:
[origin.headers]hide = ["x-internal-debug", "x-envoy-*"]An [origin.headers] only list that keeps a forwarding header while the strip is on
refuses the deployment, because it asks to keep a header the strip removes. That applies to
an entry naming one (x-forwarded-for, True-Client-IP) and to the pattern x-forwarded-*. A
broader pattern such as x-* is accepted: it keeps your other x- headers, and the forwarding
headers among them are still removed.
Passing them through
Section titled “Passing them through”With origin.forwarded_headers = "pass", the headers reach your origin exactly as the client sent
them, and both problems above are yours to handle: do not treat the values as facts about the
connection, and put any header your response depends on into the cache key with a
[[cache.policy.vary]] entry of the same name:
[origin]forwarded_headers = "pass"
[[cache.policy.vary]]name = "x-forwarded-host"value = "header:x-forwarded-host"pass_to_origin = "plain"The client’s address
Section titled “The client’s address”Read the client’s address from gp-client-ip, which no client can set. Turn on
origin.forward_client_ip and trust the header on a request
whose signature verifies. It is independent of
origin.forwarded_headers: it is sent in both modes.
Request ids
Section titled “Request ids”Every request is recorded under an id, shown as the Request ID column in Insights and as Entry ID in the request’s Overview tab. Two independent settings decide who else learns it:
| Setting | Who sees the id | Rules |
|---|---|---|
response. | The caller, as gp- | Off by default. |
origin. | Your origin, under the name you choose, commonly x-request-id | A gp- name, or one that is not a valid header name, refuses the deployment. An [origin. only list does not remove it. A client-sent header of the same name is replaced. Do not reuse the name of your telemetry., or the client’s correlation id is replaced on its way to your origin. |
An id GraphPilot reported that your origin has no record of is the normal case for a fresh cache
hit, not a lost request. gp-cache tells the two apart. Following one request across both logs is
on correlating logs.
Reserved path
Section titled “Reserved path”| Path | What GraphPilot does |
|---|---|
POST /__gp/purge | Reserved: answered by GraphPilot itself, on every hostname of the service, and never forwarded to your origin. A request GraphPilot did not send is answered with 401 and {"error":"unauthorized"}, and counts as one request unit. To purge, use the API or CLI. Only POST to exactly this path is reserved; other methods and other paths under /__gp/ are forwarded as usual. |
CORS preflights, and the methods you never cache
Section titled “CORS preflights, and the methods you never cache”A browser sends an OPTIONS preflight before a cross-origin POST.
cache.policy.uncacheable_methods holds OPTIONS by
default, so preflights skip the cache and reach your origin (CACHE_PASS_UNCACHEABLE_METHOD).
With uncacheable_methods = [] | Detail |
|---|---|
| A preflight becomes a cache candidate | It still needs a response policy that permits storing it. A preflight answered without a Cache- is not stored. |
| Keyed per question | GraphPilot adds Origin, Access- and Access- to the entry’s vary rule, whatever your origin’s own Vary says, so one site is never handed the answer computed for another. Every browser on one site asking the same question reads the same entry. |
| Revocation | A cached preflight keeps saying yes for its freshness lifetime. Revoking a site’s access means purging or waiting it out, the same question as authenticated requests. |
| Little to win | When your origin’s Access- is already generous, browsers cache the preflight themselves and the edge sees few. |
| Method names | Written in capitals. A name that is not a valid HTTP method is refused when the configuration is read. |
Edge rejections
Section titled “Edge rejections”A response GraphPilot produces itself carries none of the CORS headers your origin would have
sent, so a browser blocks it and your app sees a network error instead of the code. That covers
every response GraphPilot writes instead of your origin’s: the rejections in
codes (such as a 400, 401, 403, 404, 405,
413 or 415), the 500, 502 and 504 GraphPilot answers when it cannot get a response from
your origin, and the persisted query answers PERSISTED_QUERY_NOT_FOUND and
PERSISTED_QUERY_NOT_SUPPORTED. List the origins that may read them in
[response.cors]:
[response.cors]allow_origins = ["https://app.example.com"]allow_credentials = falseWhen a request’s Origin matches a listed origin (case-insensitively), a response GraphPilot
produced itself carries:
| Header | Value |
|---|---|
Access- | The request’s own Origin, or * when the list is ["*"] |
Vary | Origin |
Access- | true, only with allow_credentials = true |
Access- | gp-, only with response. on |
A request without an Origin, or from an origin not on the list, gets none of them. A response
from your origin, a cache hit included, carries exactly the CORS headers your origin sent, and a
preflight (OPTIONS) still goes to your origin.
Related
Section titled “Related”- Troubleshooting: the checks that read these headers.
- Cache statuses: what
HIT,MISS,PASSandSTALEmean. - Codes: every value
gp-cache-reasoncan carry. - Request signing: verifying
gp-signatureon your origin.