Apollo Server v4 static persisted queries failing after transition from automatic local mode
0 reputation · 19 Apr 2024, 13:17 UTC
0 reputation · 19 Apr 2024, 13:17 UTC
Transitioning from automatic persisted queries to static persisted queries in Apollo Server v4 causes production 'Query not found' errors. In development, the server successfully caches hashes on-the-fly when the client sends full query strings. However, the production environment is configured to rely on a pre-computed mapping file to restrict arbitrary query execution.
Despite having the client-side build manifest containing the correct hashes, the production server fails to resolve operations when the client sends only the persistedQuery hash. This mismatch occurs even when the server de-serialization store is initialized with the manifest and the Redis cache is properly populated during server startup.
Make sure the persistedQueries plugin is explicitly registered with resolveRequest: true and that the server loads the build‑time manifest (the JSON mapping of query strings to SHA‑256 hashes) before it starts accepting requests. When this is done, the server’s de‑serialization store will contain the exact hashes the client sends, and the 'Query not found' errors disappear.
resolveRequest: true tells Apollo Server to look up the hash in the static map and reject unknown hashes with PersistedQueryNotFound.loadPersistedQueries option (or by passing the object directly to persistedQueries).const { ApolloServer } = require('@apollo/server');
const { expressMiddleware } = require('@apollo/server/express4');
const { PersistedQueryPlugin } = require('@apollo/server/plugin/persistedQuery');
const server = new ApolloServer({
schema: yourSchema,
plugins: [
PersistedQueryPlugin({
// <-- essential for static mode
resolveRequest: true,
// load the manifest generated by your client build
loadPersistedQueries: () => require('./path/to/persistedQueries.json'),
}),
],
});
PersistedQueryNotFound messages. If they disappear, the map is correctly synchronized.extensions (JSON‑encoded) or via a custom header like Apollo-Require-Persisted-Query (depending on your client). Ensure the balancer forwards the full query string and does not strip unknown parameters.extensions.persistedQuery. Confirm the balancer does not parse and discard the body or rewrite the Content‑Type.curl -X POST https://your-api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"extensions":{"persistedQuery":{"version":1,"sha256Hash":""}}}'
Are you using GET or POST to transmit the persisted query hash? The header/query‑string handling differs between the two, and the load‑balancer fix depends on knowing which transport you rely on.
Use comments to ask for clarification. Post a solution as an answer.
1,850 reputation · 19 Apr 2024, 19:56 UTC
In a distributed deployment each Apollo Server instance must read the persisted‑query manifest before it begins accepting connections; otherwise the de‑serialization store starts empty and every hash lookup fails with PersistedQueryNotFound.
const { ApolloServer } = require('@apollo/server');
const { PersistedQueryPlugin } = require('@apollo/server/plugin/persistedQuery');
const server = new ApolloServer({
schema: yourSchema,
plugins: [
PersistedQueryPlugin({
resolveRequest: true,
// Load synchronously so the store is populated before `server.start()`
loadPersistedQueries: () => require('./persistedQueries.json'),
}),
],
});
await server.start(); // manifest already loaded
Additionally, verify that your load balancer or reverse proxy does not strip the custom header Apollo Client uses for the hash handshake (typically Apollo-Require-Persisted-Query or Persisted-Query). If the header is removed, the server never receives the hash and treats the request as a full query, which static mode rejects.
You can confirm header preservation by logging incoming headers at startup:
server.applyMiddleware({ app, path: '/graphql' });
app.use((req, res, next) => {
console.log('Incoming headers:', req.headers);
next();
});
When the manifest is loaded and the header is present, the response will include extensions.persistedQuery.sha256 instead of an error.