Skip to content

Serve query operations over GET with persisted operations, and emit HTTP caching headers #805

Description

@ddeboer

HTTP caching is keyed on method and URL. POST /graphql with the operation in the body has one URL for every query and the body is not part of the cache key, so ETag, If-None-Match, Last-Modified and Cache-Control on a POST response are inert – no browser cache, CDN or shared proxy stores or revalidates it. The NDE Stack notes this as “an edge the single POST /graphql endpoint” lacks (layers/platform.md, REST API). The search API currently offers no cacheable path.

Proposal

Serve query operations over GET with persisted operations, and emit HTTP caching headers on those responses.

  1. GET for query operations. The GraphQL spec permits GET /graphql?query=…&variables=… for queries (not mutations); graphql-yoga (ADR 14) already handles it. Document it as the cacheable path.
  2. Persisted operations. Full queries are too long for URLs and defeat cache keys by whitespace. With persisted operations a client sends GET /graphql?extensions={"persistedQuery":{"sha256Hash":"…"}}&variables={…} – a short, stable URL per operation and variables. graphql-yoga’s @graphql-yoga/plugin-persisted-operations supports it; the operations can be extracted from a consumer’s committed documents at build time (LOL already commits schema.graphql; the client documents are the natural next artefact). Allow-listing is a bonus: a deployment can refuse arbitrary operations from the public internet while keeping the playground open.
  3. Caching headers on GET responses, per the NDE generic API specification’s caching chapter, which is the well-specified text for this: Cache-Control (a deployment-chosen max-age), ETag, Last-Modified, 304 on If-None-Match / If-Modified-Since with If-None-Match taking precedence, and Vary: Accept-Language (Search API varies by Accept-Language without sending Vary or Content-Language #800) plus Vary: Accept.
  4. What the validator means. The honest ETag is a hash of the response body – cheap, exact, and independent of writer mode. Last-Modified needs a source: the collection’s last successful commit for a blue/green deployment (one moment for every representation); for an In-place deployment the newest last_seen/indexed-at among the documents in the response, since documents change continuously beneath a long-lived collection and there is no single invalidation moment. State which one a deployment uses. (Schema/index migration support: keep SearchSchema and the search index from drifting during a deploy #531 – keeping schema and index from drifting during a deploy – is adjacent.)

Not in scope

  • Per-entity URLs: GraphQL has none; entity-level caching stays with the client’s normalised cache keyed by id, which is one more reason id must be the stable IRI.
  • Response-level Cache-Control on POST: leave it absent rather than pretend.

Context

From the review of the NDE generic API specification against LDE and LOL. The specification’s HTTP chapters – caching, rate limiting, CORS, client identification, versioning, problem details – are its strongest part and transport-agnostic; rate limiting, CORS, User-Agent and API-Version apply to POST /graphql as they are, but caching applies to a GraphQL API only via GET with persisted operations. This issue is that step.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions