Persisted Queries

Sometimes you want to confine an API to only fire a set of pre-defined queries. This can be useful to not only reduce the amount of information transferred over the wire but also act as a list of approved queries, providing additional security. Regardless of your motivation, the approach involves associating a known string with a particular query and sending that string to the server instead of the full query body. To support this, houdini provides a query’s hash to the fetch function for you to use.

Fixed List

The simplest solution for persisted queries it to generate a fixed mapping of hash to query for every document that your client will send.

Two ways to generate this list:

  1. Configuring the persistedQueriesPath option in your houdini.config.js file. More info in the config section.
  2. Running the generate command with the --output flag and provide a path to save the map:
npx houdini generate --output ./queries.json
# or
npx houdini generate -o ./queries.json

Assuming you’ve made this list available to your server somehow, you can now simply pass the hash under whatever field name your API is configured to use instead of sending the full operation text with every request:

src/client.ts
export default new HoudiniClient({
    url: 'http://localhost:4000',
    fetchParams({ hash, variables }){
        return {
            body: JSON.stringify({
                doc_id: hash,
                variables: variables
            })
        }
    }
})
src/client.js
export default new HoudiniClient({
    url: 'http://localhost:4000',
    fetchParams({ hash, variables }) {
        return {
            body: JSON.stringify({
                doc_id: hash,
                variables: variables,
            }),
        }
    },
})

Automatic

An approach to Persisted Queries, popularized by Apollo, is known as Automatic Persisted Queries (APQ). This involves first sending a query’s hash and if its unrecognized, sending the full query string. The easiest way to do this is to define a client plugin. This might look something like:

src/client.ts
import { HoudiniClient } from '$houdini'
import type { ClientPlugin } from '$houdini'

export default new HoudiniClient({
    url: 'localhost:4000/graphql',
    // by default, send the hash and variables
    fetchParams({ variables, hash }) {
        return {
            body: JSON.stringify({
                variables,
                extensions: {
                    persistedQuery: {
                        version: 1,
                        sha256Hash: hash
                    }
                }
            })
        }
    },
    plugins: [
        // but we'll retry the request if we identify a missing hash
        retryPlugin
    ]
})

// if the response contains an error indicating a missing hash, we
// need to try again with the full payload
function retryPlugin() {
    return {
        afterNetwork(ctx, { marshalVariables, value, next, resolve }) {
            // if there are no errors, we're good to move on
            if (!value.errors) {
                return resolve(ctx)
            }

            // there was an error, check if it indicates a missing hash
            if (value.errors.some(isMissingHashError)) {
                // try again with the query text
                ctx.fetchParams = {
                    ...ctx.fetchParams,
                    body: JSON.stringify({
                        text: ctx.text,
                        variables: marshalVariables(ctx),
                        extensions: {
                            persistedQuery: {
                                version: 1,
                                sha256Hash: hash
                            }
                        }
                    })
                }

                return next(ctx)
            }

            // the error does not indicate there was a missing
            // hash so resolve the request with the error
            return resolve(ctx)
        }
    }
}
src/client.js
import { HoudiniClient } from '$houdini'

export default new HoudiniClient({
    url: 'localhost:4000/graphql',
    // by default, send the hash and variables
    fetchParams({ variables, hash }) {
        return {
            body: JSON.stringify({
                variables,
                extensions: {
                    persistedQuery: {
                        version: 1,
                        sha256Hash: hash,
                    },
                },
            }),
        }
    },
    plugins: [
        // but we'll retry the request if we identify a missing hash
        retryPlugin,
    ],
})

// if the response contains an error indicating a missing hash, we
// need to try again with the full payload
function retryPlugin() {
    return {
        afterNetwork(ctx, { marshalVariables, value, next, resolve }) {
            // if there are no errors, we're good to move on
            if (!value.errors) {
                return resolve(ctx)
            }

            // there was an error, check if it indicates a missing hash
            if (value.errors.some(isMissingHashError)) {
                // try again with the query text
                ctx.fetchParams = {
                    ...ctx.fetchParams,
                    body: JSON.stringify({
                        text: ctx.text,
                        variables: marshalVariables(ctx),
                        extensions: {
                            persistedQuery: {
                                version: 1,
                                sha256Hash: hash,
                            },
                        },
                    }),
                }

                return next(ctx)
            }

            // the error does not indicate there was a missing
            // hash so resolve the request with the error
            return resolve(ctx)
        },
    }
}