PersistedQueryLink
Secure your graph while minimizing request latency.
PersistedQueryLink is a non-terminating link that enables the use of
persisted queries, a technique that reduces bandwidth by sending query hashes
instead of full query strings.
1 import { PersistedQueryLink } from "@apollo/client/link/persisted-queries";
2 import { sha256 } from "crypto-hash";
3
4 const link = new PersistedQueryLink({
5 sha256: (queryString) => sha256(queryString),
6 });Constructor signature
1constructor(
2 options: PersistedQueryLink.Options
3): PersistedQueryLinkProblems to solve
Unlike REST APIs that use a fixed URL to load data, GraphQL provides a rich query language that can be used to express the shape of application data requirements. This is a marvelous advancement in technology, but it comes at a cost: GraphQL query strings are often much longer than REST URLS—in some cases by many kilobytes.
In practice we've seen GraphQL query sizes ranging well above 10 KB just for the query text. This is significant overhead when compared with a simple URL of 50-100 characters. When paired with the fact that the uplink speed from the client is typically the most bandwidth-constrained part of the chain, large queries can become bottlenecks for client performance.
Malicious actors can exploit GraphQL APIs by sending large and complex requests that overwhelm servers and disrupt services. These attackers can abuse GraphQL's query flexibility to create deeply nested, resource-intensive queries that lead to excessive data fetching.
Solutions
Apollo supports two separate but related features called automatic persisted queries (APQs) and persisted queries. With both features, clients can execute a GraphQL operation by sending an operation's ID instead of the entire operation string. An operation's ID is a hash of the full operation string. Querying by ID can significantly reduce latency and bandwidth usage for large operation strings.
Differences between persisted queries and APQ
The persisted queries feature requires operations to be registered in a persisted query list (PQL). This allows the PQL to act as an operation safelist made by your first-party apps. As such, persisted queries is a security feature as much as a performance one.
With APQs, if the server can't find the operation ID the client provides, the server returns an error indicating that it needs the full operation string. If an Apollo client receives this error, it automatically retries the operation with the full operation string.
If you only want to improve request latency and bandwidth usage, APQ addresses your use case. If you also want to secure your supergraph with operation safelisting, you should register operations in a PQL.
For more details on differences between persisted queries and APQ, see the GraphOS persisted queries documentation.
Implementation steps
Because persisted queries requires you to preregister operations, it has additional implementation steps.
We recommend you follow this order while implementing:
| Implementation Step | Required for PQs? | Required for APQs? |
|---|---|---|
| 1. Generate the operation manifest | ✅ | -- |
| 2. Publish the operation manifest to a PQL | ✅ | -- |
| 3. Enable persisted queries on the client when it makes requests | ✅ | ✅ |
The rest of this article details these steps.
Persisted queries also require you to create and link a PQL, and to configure your router to receive persisted query requests. This document only describes the steps that need to be taken by the client to create a manifest of the client's operations and send persisted query requests. For more information on the other configuration aspects of persisted queries, see the GraphOS persisted queries documentation.
0. Requirements
Using persisted queries for safelisting has the following requirements:
Apollo Client Web (v3.7.0+)
GraphOS Router (v1.25.0+)
You can use APQ with the following versions of Apollo Client Web, Apollo Server, and Apollo Router Core:
Apollo Client Web (v3.2.0+)
Apollo Server (v1.0.0+)
Apollo Router Core (v0.1.0+)
1. Generate operation manifests
An operation manifest acts as a safelist the GraphOS Router can check incoming requests against.
You can generate the manifest using the @apollo/generate-persisted-query-manifest package:
Install the
@apollo/generate-persisted-query-manifestpackage as a dev dependency:
1npm install --save-dev @apollo/generate-persisted-query-manifestThen use its CLI to extract queries from your app:
1npx generate-persisted-query-manifestThe resulting operation manifest looks something like this:
1{
2 "format": "apollo-persisted-query-manifest",
3 "version": 1,
4 "operations": [
5 {
6 "id": "e0321f6b438bb42c022f633d38c19549dea9a2d55c908f64c5c6cb8403442fef",
7 "body": "query GetItem { thing { __typename } }",
8 "name": "GetItem",
9 "type": "query"
10 }
11 ]
12}You can optionally create a configuration file in the root of your project to override the default options. Refer to the package's README for details.
To automatically update the manifest for each new app release, include the generate-persisted-query-manifest command in your CI/CD pipeline.
id generated in the manifest and the one your
client is sending at runtime, read the options available in the
@apollo/generate-persisted-query-manifest
package to ensure the configuration matches your usage of Apollo Client.2. Publish manifests to a PQL
0.17.2 or later. Previous versions of Rover don't support publishing operations to a PQL.
Download the latest version.After you generate an operation manifest, you publish it to your PQL with the Rover CLI like so:
rover persisted-queries publish my-graph@my-variant \
--manifest ./persisted-query-manifest.jsonThe
my-graph@my-variantargument is thegraph refof any variant the PQL is linked to.Graph refs have the format
graph-id@variant-name.
Use the
--manifestoption to provide the path to the manifest you want to publish.
persisted-queries publish command assumes manifests are in the format generated by Apollo client tools. The command can also support manifests generated by the Relay compiler by adding the --manifest-format relay argument. Your Rover CLI version must be 0.19.0 or later to use this argument.The persisted-queries publish command does the following:
Publishes all operations in the provided manifest file to the PQL linked to the specified variant, or to the specified PQL.
Publishing a manifest to a PQL is additive. Any existing entries in the PQL remain.
If you publish an operation with the same
idbut different details from an existing entry in the PQL, the entire publish command fails with an error.
Updates any other variants that the PQL is applied to so that routers associated with those variants can fetch their updated PQL.
As with generating manifests, it's best to execute this command in your CI/CD pipeline to publish new operations as part of your app release process. The API key you supply to Rover must have the role of Graph Admin or Persisted Query Publisher. Persisted Query Publisher is a special role designed for use with the rover persisted-queries publish command; API keys with this role have no other access to your graph's data in GraphOS, and are appropriate for sharing with trusted third party client developers who should be allowed to publish operations to your graph's PQL but should not otherwise have access to your graph.
Test operations
You can send some test operations to test that you've successfully published your manifests:
First, start your GraphOS-connected router:
APOLLO_KEY="..." APOLLO_GRAPH_REF="..." ./router --config ./router.yaml
2023-05-11T15:32:30.684460Z INFO Apollo Router v1.18.1 // (c) Apollo Graph, Inc. // Licensed as ELv2 (https://go.apollo.dev/elv2)
2023-05-11T15:32:30.684480Z INFO Anonymous usage data is gathered to inform Apollo product development. See https://go.apollo.dev/o/privacy for details.
2023-05-11T15:32:31.507085Z INFO Health check endpoint exposed at http://127.0.0.1:8088/health
2023-05-11T15:32:31.507823Z INFO GraphQL endpoint exposed at http://127.0.0.1:4000/ 🚀Next, make a POST request with curl, like so:
curl http://localhost:4000 -X POST --json \
'{"extensions":{"persistedQuery":{"version":1,"sha256Hash":"dc67510fb4289672bea757e862d6b00e83db5d3cbbcfb15260601b6f29bb2b8f"}}}'If your router's PQL includes an operation with an ID that matches the value of the provided sha256Hash property, it executes the corresponding operation and returns its result.
3. Enable persisted queries on ApolloClient
You use the persisted queries Apollo Link to send operations as IDs rather than full operation strings. The implementation details depend on whether you're using persisted queries or APQs.
Persisted queries implementation
The persisted queries link is included in the @apollo/client package:
1npm install @apollo/clientA persisted queries implementation also requires the @apollo/persisted-query-lists package. This package contains helpers that work with the persisted queries link.
Install the @apollo/persisted-query-lists package:
1npm install @apollo/persisted-query-listsOne of the package's utilities, generatePersistedQueryIdsFromManifest, reads operation IDs from your operation manifest so the client can use them to make requests. To do so, pass the loadManifest option a function that returns your manifest. We recommend using a dynamic import to avoid bundling the manifest configuration with your production build.
1generatePersistedQueryIdsFromManifest({
2 loadManifest: () => import("./path/to/persisted-query-manifest.json"),
3});Finally, combine the link that generatePersistedQueryIdsFromManifest returns with ApolloClient's HttpLink. The easiest way to use them together is to concat them into a single link.
1import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";
2import { generatePersistedQueryIdsFromManifest } from "@apollo/persisted-query-lists";
3import { PersistedQueryLink } from "@apollo/client/link/persisted-queries";
4
5const persistedQueryLink = new PersistedQueryLink(
6 generatePersistedQueryIdsFromManifest({
7 loadManifest: () => import("./path/to/persisted-query-manifest.json"),
8 })
9);
10
11const client = new ApolloClient({
12 cache: new InMemoryCache(),
13 link: persistedQueriesLink.concat(httpLink),
14});By including the persisted queries link in your client instantiation, your client sends operation IDs from your manifest instead of the full operation string.
The @apollo/persisted-query-lists package includes additional helpers you can use to verify that you've properly configured your operation manifest and generate operation IDs at runtime. Runtime generation is slower than fetching operation IDs from the manifest, but doesn't require making your manifest available to your client.
Refer to the package README for more information.
APQ implementation
The persisted queries Apollo Link used for APQs is included in the @apollo/client package:
1npm install @apollo/clientThis link requires but doesn't include a SHA-256 hash function. It does this to avoid forcing a particular hash function as a dependency. Developers should pick the most appropriate SHA-256 function (sync or async) for their needs and environment.
If you don't already have a SHA-256 based hashing function available in your application, install one separately. For example:
1npm install crypto-hashThe link requires using ApolloClient's HttpLink. The easiest way to use them together is to concat them into a single link.
1import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";
2import { PersistedQueryLink } from "@apollo/client/link/persisted-queries";
3import { sha256 } from "crypto-hash";
4
5const httpLink = new HttpLink({ uri: "/graphql" });
6const persistedQueriesLink = new PersistedQueryLink({ sha256 });
7const client = new ApolloClient({
8 cache: new InMemoryCache(),
9 link: persistedQueriesLink.concat(httpLink),
10});Thats it! By including the persisted queries link in your client instantiation, your client sends operation IDs instead of the full operation string. This results in improved network performance, but doesn't include the security benefits of operation safelisting that persisted queries provide.
PersistedQueryLink options
The PersistedQueryLink class takes a configuration object:
sha256: a SHA-256 hashing function. Can be sync or async. Providing a SHA-256 hashing function is required, unless you're defining a fully custom hashing approach viagenerateHash.generateHash: an optional function that takes the query document and returns the hash. If provided this custom function will override the default hashing approach that uses the suppliedsha256function. If not provided, the persisted queries link will use a fallback hashing approach leveraging thesha256function.useGETForHashedQueries: set totrueto use the HTTPGETmethod when sending the hashed version of queries (but not for mutations).GETrequests are not compatible with@apollo/client/link/batch-http.If you want to use
GETfor non-mutation queries whether or not they are hashed, passuseGETForQueries: trueoption toHttpLinkinstead. If you want to useGETfor all requests, passfetchOptions: {method: 'GET'}toHttpLink.disable: a function which takes aPersistedQueryLink.DisableFunctionOptionsobject and returns a boolean to disable any future persisted queries for that session. This defaults to disabling onPersistedQueryNotSupportederror.retry: a function which takes aPersistedQueryLink.RetryFunctionOptionsobject and returns a boolean to retry the request with the full query text included. This defaults totrueonPersistedQueryNotSupportedorPersistedQueryNotFounderrors.
Apollo Studio
Apollo Studio supports receiving and fulfilling APQs. Simply adding this link into your client app will improve your network response times when using Apollo Studio.
Protocol
APQs are made up of three parts: the query signature, error responses, and the negotiation protocol.
Query Signature
The query signature for APQs is sent through the extensions field of a request from the client. This is a transport independent way to send extra information along with the operation.
1{
2 operationName: 'MyQuery',
3 variables: null,
4 extensions: {
5 persistedQuery: {
6 version: 1,
7 sha256Hash: hashOfQuery
8 }
9 }
10}When sending an Automatic Persisted Query, the client omits the query field normally present, and instead sends an extension field with a persistedQuery object as shown above. The hash algorithm defaults to a sha256 hash of the query string.
If the client needs to register the hash, the query signature will be the same but include the full query text like so:
1{
2 operationName: 'MyQuery',
3 variables: null,
4 query: `query MyQuery { id }`,
5 extensions: {
6 persistedQuery: {
7 version: 1,
8 sha256Hash: hashOfQuery
9 }
10 }
11}This should only happen once across all clients when a new query is introduced into your application.
Error Responses
When the initial query signature is received by a backend, if it is unable to find the hash previously stored, it will send back the following response signature:
1{
2 errors: [{ message: "PersistedQueryNotFound" }];
3}If the backend doesn't support APQs, or does not want to support it for that particular client, it can send back the following which will tell the client to stop trying to send hashes:
1{
2 errors: [
3 { message: 'PersistedQueryNotSupported' }
4 ]
5}Negotiation Protocol
In order to support APQs, the client and server must follow the negotiation steps as outlined here:
Happy Path
Client sends query signature with no
queryfieldServer looks up query based on hash, if found, it resolves the data
Client receives data and completes request
Missing hash path
Client sends query signature with no
queryfieldServer looks up query based on hash, none is found
Server responds with NotFound error response
Client sends both hash and query string to Server
Server fulfills response and saves query string + hash for future lookup
Client receives data and completes request
Build time generation
If you want to avoid hashing in the browser, you can use a build script to include the hash as part of the request, then pass a function to retrieve that hash when the operation is run. This works well with projects like GraphQL Persisted Document Loader which uses webpack to generate hashes at build time.
If you use the above loader, you can pass { generateHash: ({ documentId }) => documentId } to the PersistedQueryLink class.
Types
Options passed to the disable function when a persisted query request
fails.
ErrorLikeThe error that occurred during the request.
PersistedQueryLink.ErrorMetaMetadata about the persisted query error.
ApolloLink.OperationThe GraphQL operation that failed.
FormattedExecutionResultThe GraphQL result, if available.
Metadata about persisted query errors extracted from the response.
booleanWhether the server responded with a "PersistedQueryNotFound" error.
When true, indicates the server doesn't recognize the query hash
and needs the full query text.
booleanWhether the server responded with a "PersistedQueryNotSupported" error.
When true, indicates the server doesn't support persisted queries
or has disabled them for this client.
A function that generates a hash for a GraphQL document.
Example
1 import { print } from "graphql";
2 import { sha256 } from "crypto-hash";
3
4 const link = new PersistedQueryLink({
5 generateHash: async (document) => {
6 const query = print(document);
7 return sha256(query);
8 },
9 });Signature
1GenerateHashFunction(
2 document: DocumentNode
3): string | PromiseLike<string>Parameters
Options for using custom hash generation with persisted queries.
Use this configuration when you need custom control over how query hashes are generated (e.g., using pre-computed hashes).
(options: PersistedQueryLink.DisableFunctionOptions) => booleanA function to disable persisted queries for the current session.
This function is called when an error occurs and determines whether to disable persisted queries for all future requests in this session.
PersistedQueryLink.GenerateHashFunctionA custom function for generating query hashes. This function receives the GraphQL document and should return a hash. Useful for custom hashing strategies or when using build-time generated hashes.
(options: PersistedQueryLink.RetryFunctionOptions) => booleanA function to determine whether to retry a request with the full query.
When a persisted query fails, this function determines whether to retry the request with the full query text included.
neverWhether to use HTTP GET for hashed queries (excluding mutations).
GET for non-mutation queries whether or not they
are hashed, pass useGETForQueries: true option to HttpLink
instead. If you want to use GET for all requests, pass fetchOptions: {method: 'GET'}
to HttpLink.Options passed to the retry function when a persisted query request
fails.
ErrorLikeThe error that occurred during the request.
PersistedQueryLink.ErrorMetaMetadata about the persisted query error.
ApolloLink.OperationThe GraphQL operation that failed.
FormattedExecutionResultThe GraphQL result, if available.
Options for using SHA-256 hashing with persisted queries.
Use this configuration when you want the link to handle query printing and hashing using a SHA-256 function.
(options: PersistedQueryLink.DisableFunctionOptions) => booleanA function to disable persisted queries for the current session.
This function is called when an error occurs and determines whether to disable persisted queries for all future requests in this session.
(options: PersistedQueryLink.RetryFunctionOptions) => booleanA function to determine whether to retry a request with the full query.
When a persisted query fails, this function determines whether to retry the request with the full query text included.
PersistedQueryLink.SHA256FunctionThe SHA-256 hash function to use for hashing queries. This function receives the printed query string and should return a SHA-256 hash. Can be synchronous or asynchronous.
Whether to use HTTP GET for hashed queries (excluding mutations).
GET for non-mutation queries whether or not they
are hashed, pass useGETForQueries: true option to HttpLink
instead. If you want to use GET for all requests, pass fetchOptions: {method: 'GET'}
to HttpLink.