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:
- Configuring the
persistedQueriesPath
option in yourhoudini.config.js
file. More info in the config section. - 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:
export default new HoudiniClient({
url: 'http://localhost:4000',
fetchParams({ hash, variables }){
return {
body: JSON.stringify({
doc_id: hash,
variables: variables
})
}
}
})
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:
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)
}
}
}
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)
},
}
}