Skip to content

Commit 47a0e12

Browse files
author
Stephen Barlow
committed
Initial edits to APQ docs
1 parent d90ac58 commit 47a0e12

2 files changed

Lines changed: 70 additions & 41 deletions

File tree

docs/source/performance/apq.md

Lines changed: 66 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -3,78 +3,103 @@ title: Automatic persisted queries
33
description: Improve network performance by sending smaller requests
44
---
55

6-
The size of individual GraphQL query strings can be a major pain point. Apollo Server implements Automatic Persisted Queries (APQ), a technique that greatly improves network performance for GraphQL with zero build-time configuration. A persisted query is an ID or hash that can be sent to the server instead of the entire GraphQL query string. This smaller signature reduces bandwidth utilization and speeds up client loading times. Persisted queries are especially nice paired with `GET` requests, enabling the browser cache and [integration with a CDN](#using-get-requests-with-apq-on-a-cdn).
6+
Clients send queries to Apollo Server as HTTP requests that include the GraphQL string of the query to execute. Depending on your graph's schema, the size of a valid query string might be arbitrarily large. As query strings become larger, increased latency and network usage can noticeably degrade client performance.
7+
8+
To improve network performance for large query strings, Apollo Server supports **Automatic Persisted Queries** (**APQ**). A persisted query is a query string that's stored on the server side, along with its unique identifier (always its SHA-256 hash). Clients can send this identifier _instead of_ the corresponding query string, thus reducing request sizes dramatically (response sizes are unaffected).
9+
10+
To persist a query string, Apollo Server must first receive it from a requesting client. Consequently, the _first_ execution of each unique query does _not_ benefit from APQ.
11+
12+
```mermaid
13+
sequenceDiagram;
14+
Client app->>Apollo Server: Sends SHA-256 hash of query string to execute
15+
Note over Apollo Server: Fails to find persisted query string
16+
Apollo Server->>Client app: Responds with error
17+
Client app->>Apollo Server: Sends both query string AND hash
18+
Note over Apollo Server: Persists query string and hash
19+
Apollo Server->>Client app: Executes query and returns result
20+
Note over Client app: Time passes
21+
Client app->>Apollo Server: Sends SHA-256 hash of query string to execute
22+
Note over Apollo Server: Finds persisted query string
23+
Apollo Server->>Client app: Executes query and returns result
24+
```
25+
26+
Persisted queries are especially effective when clients send queries as `GET` requests. This enables clients to take advantage of the browser cache and [integrate with a CDN](#using-get-requests-with-apq-on-a-cdn).
27+
28+
Because query identifiers are deterministic hashes, clients can generate them at runtime. No additional build steps are required.
29+
30+
## Apollo Client setup
31+
32+
Apollo Server supports APQ without any additional configuration. However, some _client-side_ configuration is required.
733

8-
With Automatic Persisted Queries, the ID is a deterministic hash of the input query, so we don't need a complex build step to share the ID between clients and servers. If a server doesn't know about a given hash, the client can expand the query for it; Apollo Server caches that mapping.
34+
To set up APQ in Apollo Client, first import the `createPersistedQueryLink` function in the same file where you initialize `ApolloClient`:
935

10-
## Setup
36+
```js:title=index.js
37+
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
38+
```
1139

12-
Apollo Server supports automatic persisted queries without any additional configuration and only requires changes to Apollo Client.
40+
This function creates a link that you can add to your client's [Apollo Link chain](https://www.apollographql.com/docs/react/api/link/introduction/). The link takes care of generating APQ identifiers, using `GET` requests for hashed queries, and retrying requests with query strings when necessary.
1341

14-
To get started with APQ, add the [Automatic Persisted Queries Link](https://github.com/apollographql/apollo-link-persisted-queries) to the **client** codebase with `npm install apollo-link-persisted-queries`. Next incorporate the APQ link with Apollo Client's link chain before the HTTP link:
42+
Add the persisted query link anywhere in the chain before the terminating link. This example shows a basic two-link chain:
1543

1644
```js
17-
import { createPersistedQueryLink } from "apollo-link-persisted-queries";
18-
import { createHttpLink } from "apollo-link-http";
19-
import { InMemoryCache } from "apollo-cache-inmemory";
20-
import ApolloClient from "apollo-client";
45+
import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client";
46+
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";
2147

22-
const link = createPersistedQueryLink().concat(createHttpLink({ uri: "/graphql" }));
48+
const linkChain = createPersistedQueryLink().concat(new HttpLink({ uri: "http://localhost:4000/graphql" }));
2349

2450
const client = new ApolloClient({
2551
cache: new InMemoryCache(),
2652
link: link,
2753
});
2854
```
2955

30-
> Note: Users of `apollo-boost` should [migrate to `apollo-client`](https://www.apollographql.com/docs/react/advanced/boost-migration/) in order to use the `apollo-link-persisted-queries` package.
56+
## Command-line testing
3157

32-
## Verify
58+
You can test out APQ directly from the command line. This section also helps illustrate the shape of APQ requests, so you can use it to add APQ support to GraphQL clients besides Apollo Client.
3359

34-
Apollo Server's persisted queries configuration can be tested from the command-line. The following examples assume Apollo Server is running at `localhost:4000/`.
35-
This example persists a dummy query of `{__typename}`, using its sha256 hash: `ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38`.
60+
> This section assumes your server is running locally at `http://localhost:4000/graphql`.
3661
62+
Almost every GraphQL server supports the following query (which requests the `__typename` field from the `Query` type):
3763

38-
1. Request a persisted query:
64+
```graphql
65+
{__typename}
66+
```
3967

40-
```bash
41-
curl -g 'http://localhost:4000/?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
42-
```
68+
The SHA-256 hash of this query string is the following:
4369

44-
Expect a response of: `{"errors": [{"message": "PersistedQueryNotFound", "extensions": {...}}]}`.
70+
```none
71+
ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38
72+
```
4573

46-
2. Store the query to the cache:
74+
1. Attempt to execute this query on your running server by providing its hash in a `curl` command, like so:
4775

48-
```bash
49-
curl -g 'http://localhost:4000/?query={__typename}&extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
50-
```
76+
```shell
77+
curl -g 'http://localhost:4000/graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
78+
```
5179

52-
Expect a response of `{"data": {"__typename": "Query"}}"`.
80+
The first time you try this, Apollo Server responds with an error with the code `PERSISTED_QUERY_NOT_FOUND`. This tells us that Apollo Server hasn't yet received the associated query string.
5381
54-
3. Request the persisted query again:
82+
2. Send a followup request that includes both the query string _and_ its hash, like so:
5583
56-
```bash
57-
curl -g 'http://localhost:4000/?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
58-
```
84+
```shell
85+
curl -g 'http://localhost:4000/?query={__typename}&extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
86+
```
5987
60-
Expect a response of `{"data": {"__typename": "Query"}}"`, as the query string is loaded from the cache.
88+
This time, the server persists the query string and then responds with the query result as we'd expect.
6189

62-
## Using `GET` requests with APQ on a CDN
90+
> The hash you provide _must_ be the exact SHA-256 hash of the query string. If it isn't, Apollo Server returns an error.
6391
64-
A great application for APQ is running Apollo Server behind a CDN. Many CDNs only cache GET requests, but many GraphQL queries are too long to fit comfortably in a cacheable GET request. When the APQ link is created with `createPersistedQueryLink({useGETForHashedQueries: true})`, Apollo Client automatically sends the short hashed queries as GET requests allowing a CDN to serve those request. For full-length queries and for all mutations, Apollo Client will continue to use POST requests.
92+
3. Finally, attempt the request from step 1 again:
6593
66-
### How it works
94+
```shell
95+
curl -g 'http://localhost:4000/graphql?extensions={"persistedQuery":{"version":1,"sha256Hash":"ecf4edb46db40b5132295c0291d62fb65d6759a9eedfa4d5d612dd5ec54a6b38"}}'
96+
```
6797
68-
The mechanism is based on a lightweight protocol extension between Apollo Client and Apollo Server. It works as follows:
98+
This time, the server responds with the query result because it successfully located the associated query string in its cache.
6999
70-
- When the client makes a query, it will optimistically send a short (64-byte) cryptographic hash instead of the full query text.
71-
- **Optimized Path:** If a request containing a persisted query hash is detected, Apollo Server will look it up to find a corresponding query in its registry. Upon finding a match, Apollo Server will expand the request with the full text of the query and execute it.
72-
73-
<img src="../images/persistedQueries.optPath.png" width="80%" style="margin: 5%" alt="Optimized Path">
74-
75-
- **New Query Path:** In the unlikely event that the query is not already in the Apollo Server registry (this only happens the very first time that Apollo Server sees a query), it will ask the client to resend the request using the full text of the query. At that point, Apollo Server will store the query / hash mapping in the registry for all subsequent requests to benefit from.
100+
## Using `GET` requests with APQ on a CDN
76101
77-
<img src="../images/persistedQueries.newPath.png" width="80%" style="margin: 5%;" alt="New Query Path">
102+
A great application for APQ is running Apollo Server behind a CDN. Many CDNs only cache GET requests, but many GraphQL queries are too long to fit comfortably in a cacheable GET request. When the APQ link is created with `createPersistedQueryLink({useGETForHashedQueries: true})`, Apollo Client automatically sends the short hashed queries as GET requests allowing a CDN to serve those request. For full-length queries and for all mutations, Apollo Client will continue to use POST requests.
78103
79104
## CDN Integration
80105
@@ -138,7 +163,7 @@ const client = new ApolloClient({
138163
});
139164
```
140165

141-
> If you are testing locally, make sure to include the full [URI](https://developer.mozilla.org/en-US/docs/Glossary/URI) including the port number. For example: ` uri: "http://localhost:4000/graphql"`.
166+
> If you are testing locally, make sure to include the full [URI](https://developer.mozilla.org/en-US/docs/Glossary/URI) including the port number. For example: ` uri: "http://localhost:4000/graphql"`.
142167
143168
Make sure to include `useGETForHashedQueries: true`. Note that the client will still use POSTs for mutations because it's generally best to avoid GETs for non-idempotent requests.
144169

packages/apollo-server-core/src/requestPipeline.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,10 @@ export async function processGraphQLRequest<TContext>(
200200
} else {
201201
const computedQueryHash = computeQueryHash(query);
202202

203+
// The provided hash must exactly match the SHA-256 hash of
204+
// the query string. This prevents hash hijacking, where a
205+
// new and potentially malicious query is associated with
206+
// an existing hash.
203207
if (queryHash !== computedQueryHash) {
204208
// We are returning to `runHttpQuery` to preserve legacy behavior while
205209
// still delivering observability to the `didEncounterErrors` hook.

0 commit comments

Comments
 (0)