GraphQL Context Propagation#
Strapi's GraphQL plugin propagates root query parameters — primarily status (draft/published) and locale — down through nested association resolvers so that nested fields return documents consistent with the root query's intent. Without this, a query for published content could silently return draft nested relations, or two aliased root fields with different statuses could contaminate each other's nested results.
How It Works#
Context propagation operates in two phases.
Phase 1 — Capture root args (bootstrap): An Apollo server plugin registered in bootstrap.ts fires a willResolveField hook for every top-level query field. When the resolver source is absent (i.e., a root field, not a nested one) and the operation is a query, the plugin stores the field's args in context.rootQueryArgsByPath — a Map<string | number, Record<string, unknown>> keyed by info.path.key (the alias or field name). The entry also records _originField: info.fieldName to identify which built-in query field was called .
Phase 2 — Consume inherited args (association resolver): The association resolver walks info.path back to the root to determine which branch it belongs to, then looks up context.rootQueryArgsByPath.get(rootPath.key). It then checks whether _originField refers to a built-in query (via isBuiltInQueryField). If so, it inherits status (defaulting to 'published' if absent) and applies a publishedAt filter to the database query. Custom resolvers are explicitly excluded from inheritance to prevent conflicts.
The combined database query is assembled as :
merge(merge(defaultFilters, publicationFilterWhere), transformedQuery)
where defaultFilters holds the inherited publishedAt condition and publicationFilterWhere holds any publication-filter predicate (e.g., has-published-version-document).
Resolver Coverage#
| Resolver | File | Inherits root args? |
|---|---|---|
| Association (relations, media) | resolvers/association.ts | ✅ Full (status + publication filter) |
| Component | resolvers/component.ts | ❌ Loads via db.query().load() only |
| Dynamic zone | resolvers/dynamic-zone.ts | ❌ Bare db.query().load() — no filtering |
Components and dynamic zones load nested data directly from the parent's already-loaded entity, so they don't need independent publication filtering. Only association resolvers issue a fresh db.query().load() that requires explicit filters.
Path-Scoped Isolation (PR #26178)#
Before PR #26178, a single shared context.rootQueryArgs was overwritten by whichever root field resolved last. This broke queries with multiple aliased fields:
{
drafts: cars(status: DRAFT) { doors { name } }
published: cars(status: PUBLISHED) { doors { name } }
}
Both doors resolvers would inherit from the same overwritten object. The fix stores args in a Map keyed by info.path.key, so each branch reads only its own root's args .
i18n Localizations Inheritance (PR #22163)#
PR #22163 extended the mechanism to i18n: localizations nested under a published document now inherit status: 'published' from the root query, matching REST API behavior. Previously, querying a published document's localizations could return draft versions from the same document tree.
The guard shouldInheritRootQueryStatus ensures this only applies when the root is a built-in query field — custom resolvers are left untouched.
Key Entry Points#
| File | Purpose |
|---|---|
bootstrap.ts | Apollo plugin that populates rootQueryArgsByPath per field |
resolvers/association.ts | Association resolver that reads and applies inherited args |
resolvers/component.ts | Component resolver — no propagation |
resolvers/dynamic-zone.ts | Dynamic zone resolver — no propagation |
services/content-api/index.ts | isBuiltInQueryField() — gates inheritance to built-in queries |