Docs
Launch GraphOS Studio

Persisted Queries Link

Secure your graph while minimizing request latency.


Problems to solve

Unlike REST APIs that use a fixed URL to load data, provides a rich 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 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 APIs by sending large and complex requests that overwhelm servers and disrupt services. These attackers can abuse GraphQL's 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 () and persisted queries. With both features, clients can execute a by sending an operation's ID instead of the entire operation string. An operation's ID is a hash of the full operation string. by ID can significantly reduce latency and bandwidth usage for large operation strings.

Differences between persisted queries and APQ

The feature requires to be registered in a persisted query list (). This allows the PQL to act as an safelist made by your first-party apps. As such, is a security feature as much as a performance one.

With , if the server can't find the ID the client provides, the server returns an error indicating that it needs the full operation string. If an receives this error, it automatically retries the operation with the full operation string.

If you only want to improve request latency and bandwidth usage, addresses your use case. If you also want to secure your with safelisting, you should register operations in a .

For more details on differences between and , see the GraphOS persisted queries documentation.

Implementation steps

Because requires you to preregister , it has additional implementation steps.

We recommend you follow this order while implementing:

Implementation StepRequired 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.

also require you to create and link a , and to configure your to receive requests. This only describes the steps that need to be taken by the client to create a manifest of the client's 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 for safelisting has the following requirements:

You can use with the following versions of Web, , and :

Note: You can use either or for . They don't need to be used together.

1. Generate operation manifests

This step is only required for persisted queries, not APQ.

An manifest acts as a safelist the Apollo Router can check incoming requests against. You can generate the manifest using the @apollo/generate-persisted-query-manifest package:

  1. Install the @apollo/generate-persisted-query-manifest package as a dev dependency:
npm install --save-dev @apollo/generate-persisted-query-manifest
  1. Then use its CLI to extract queries from your app:
npx generate-persisted-query-manifest

The resulting manifest looks something like this:

persisted-query-manifest.json
{
"format": "apollo-persisted-query-manifest",
"version": 1,
"operations": [
{
"id": "e0321f6b438bb42c022f633d38c19549dea9a2d55c908f64c5c6cb8403442fef",
"body": "query GetItem { thing { __typename } }",
"name": "GetItem",
"type": "query"
}
]
}

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.

2. Publish manifests to a PQL

💡 TIP

Ensure your version is 0.17.2 or later. Previous versions of don't support publishing to a . Download the latest version.

After you generate an operation manifest, you publish it to your with the Rover CLI like so:

Example command
rover persisted-queries publish my-graph@my-variant \
--manifest ./persisted-query-manifest.json
  • The my-graph@my-variant is the graph ref of any the is linked to.
    • have the format graph-id@variant-name.
  • Use the --manifest option to provide the path to the manifest you want to publish.

NOTE

The persisted-queries publish command assumes manifests are in the format generated by tools. The command can also support manifests generated by the Relay compiler by adding the --manifest-format relay . Your version must be 0.19.0 or later to use this argument.

The persisted-queries publish command does the following:

  1. Publishes all in the provided manifest file to the linked to the specified , or to the specified PQL.

    • Publishing a manifest to a is additive. Any existing entries in the PQL remain.
    • If you publish an with the same id but different details from an existing entry in the , the entire publish command fails with an error.
  2. Updates any other that the is applied to so that 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 as part of your app release process. The API key you supply to 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 's data in , and are appropriate for sharing with trusted third party client developers who should be allowed to publish to your graph's but should not otherwise have access to your graph.

Test operations

You can send some test to test that you've successfully published your manifests:

First, start your -connected :

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 's includes an with an ID that matches the value of the provided sha256Hash property, it executes the corresponding and returns its result.

3. Enable persisted queries on ApolloClient

You use the persisted queries to send as IDs rather than full operation strings. The implementation details depend on whether you're using or .

Persisted queries implementation

The link is included in the @apollo/client package:

npm install @apollo/client

A implementation also requires the @apollo/persisted-query-lists package. This package contains helpers that work with the link.

Install the @apollo/persisted-query-lists package:

npm install @apollo/persisted-query-lists

One of the package's utilities, generatePersistedQueryIdsFromManifest, reads 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.

generatePersistedQueryIdsFromManifest({
loadManifest: () => import("./path/to/persisted-query-manifest.json"),
})

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.

import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";
import { generatePersistedQueryIdsFromManifest } from "@apollo/persisted-query-lists";
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
const persistedQueryLink = createPersistedQueryLink(
generatePersistedQueryIdsFromManifest({
loadManifest: () => import("./path/to/persisted-query-manifest.json"),
}),
);
const client = new ApolloClient({
cache: new InMemoryCache(),
link: persistedQueriesLink.concat(httpLink),
});

By including the link in your client instantiation, your client sends 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 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 used for is included in the @apollo/client package:

npm install @apollo/client

This 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:

npm install crypto-hash

The link requires using ApolloClient's HttpLink. The easiest way to use them together is to concat them into a single link.

import { HttpLink, InMemoryCache, ApolloClient } from "@apollo/client";
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
import { sha256 } from 'crypto-hash';
const httpLink = new HttpLink({ uri: "/graphql" });
const persistedQueriesLink = createPersistedQueryLink({ sha256 });
const client = new ApolloClient({
cache: new InMemoryCache(),
link: persistedQueriesLink.concat(httpLink),
});

Thats it! By including the link in your client instantiation, your client sends 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.

The createPersistedQueryLink function 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 via generateHash.
  • generateHash: an optional function that takes the and returns the hash. If provided this custom function will override the default hashing approach that uses the supplied sha256 function. If not provided, the link will use a fallback hashing approach leveraging the sha256 function.
  • useGETForHashedQueries: set to true to use the HTTP GET method when sending the hashed version of queries (but not for ). GET requests are not compatible with @apollo/client/link/batch-http.

    If you want to use GET for non- 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.

  • disable: a function which takes an ErrorResponse (see below) and returns a boolean to disable any future for that session. This defaults to disabling on PersistedQueryNotSupported or a 400 or 500 http error.

ErrorResponse

The that the optional disable function is given is an object with the following keys:

  • operation: The that encountered an error (contains query, variables, operationName, and context).
  • response: The Execution of the response (contains data and errors as well extensions if sent from the server).
  • graphQLErrors: An array of errors from the endpoint.
  • networkError: Any error during the link execution or server response.

Note: networkError is the value from the downlink's error callback. In most cases, graphQLErrors is the errors of the result from the last next call. A networkError can contain additional , such as a object in the case of a failing HTTP status code from @apollo/link/http. In this situation, graphQLErrors is an for networkError.result.errors if the property exists.

Apollo Studio

Apollo Studio supports receiving and fulfilling . Simply adding this link into your client app will improve your network response times when using Apollo Studio.

Protocol

are made up of three parts: the signature, error responses, and the negotiation protocol.

Query Signature

The signature for is sent through the extensions of a request from the client. This is a transport independent way to send extra information along with the .

{
operationName: 'MyQuery',
variables: null,
extensions: {
persistedQuery: {
version: 1,
sha256Hash: hashOfQuery
}
}
}

When sending an Automatic , the client omits the query 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 string.

If the client needs to register the hash, the signature will be the same but include the full query text like so:

{
operationName: 'MyQuery',
variables: null,
query: `query MyQuery { id }`,
extensions: {
persistedQuery: {
version: 1,
sha256Hash: hashOfQuery
}
}
}

This should only happen once across all clients when a new is introduced into your application.

Error Responses

When the initial signature is received by a backend, if it is unable to find the hash previously stored, it will send back the following response signature:

{
errors: [
{ message: 'PersistedQueryNotFound' }
]
}

If the backend doesn't support , 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:

{
errors: [
{ message: 'PersistedQueryNotSupported' }
]
}

Negotiation Protocol

In order to support , the client and server must follow the negotiation steps as outlined here:

Happy Path

  1. Client sends signature with no query
  2. Server looks up based on hash, if found, it resolves the data
  3. Client receives data and completes request

Missing hash path

  1. Client sends signature with no query
  2. Server looks up based on hash, none is found
  3. Server responds with NotFound error response
  4. Client sends both hash and string to Server
  5. Server fulfills response and saves string + hash for future lookup
  6. 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 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 createPersistedQueryLink call.

Previous
Error
Next
Remove Typename
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company