Skip to content

feat(router): request selected supergraphs - #1284

Merged
enisdenjo merged 42 commits into
mainfrom
super-replace-schemastate
Jul 21, 2026
Merged

feat(router): request selected supergraphs#1284
enisdenjo merged 42 commits into
mainfrom
super-replace-schemastate

Conversation

@enisdenjo

@enisdenjo enisdenjo commented Jul 15, 2026

Copy link
Copy Markdown
Member

Supersedes and therefore closes #1269, closes #1137, addresses #524 through a plugin

Allow a plugin to select a supergraph document during on_http_request and have it hold for each HTTP request or WebSocket upgrade. The selected supergraph is used consistently across validation, introspection, normalization, planning, authorization, demand control, execution, coprocessors, usage reporting, subscriptions, and request deduplication.

The implementation separates two kinds of state that were previously bundled together:

  • Supergraph (executor) owns schema-derived data that a plugin can safely construct and retain
  • RouterSupergraphRuntime (router) owns router-specific state derived from both that supergraph and the router configuration

Configured supergraphs still build their runtime eagerly before publication. Plugin-selected supergraphs build their runtime lazily on first use and reuse it through a bounded router-owned cache.

Also adding supergraph.source: plugin, where there is deliberately no configured fallback. A plugin must select a supergraph for each request that needs one.

Executor (public)

Supergraph

Supergraph is the public owner handle. Plugins construct it with either:

Supergraph::from_sdl(...)
Supergraph::from_document(...)

Construction only accepts query-planner options. It does not accept router configuration, telemetry, callback subscription maps, HTTP clients, or other router-owned state.

The schema-derived data includes:

  • The parsed supergraph document
  • The public consumer schema and its SDL
  • The query planner and planner supergraph state
  • Schema metadata
  • A process-local cache identity

Plugins retain an Arc<Supergraph> for as long as that variant should remain selectable. OnHttpRequestHookPayload::set_supergraph immediately takes a SupergraphSnapshot and stores that snapshot in request extensions. The router does not retain the plugin's Arc<Supergraph> owner.

This distinction is important. A request snapshot keeps the immutable schema data alive, but it does not keep the owner alive. The plugin remains the authority over whether new requests can select a variant!

Retirement

Supergraph owns a CancellationToken. SupergraphSnapshot receives a clone of that token. When the last Arc<Supergraph> is dropped, Supergraph::drop cancels the token.

This gives retirement the required ownership semantics without callbacks from the executor crate into the router:

  1. The plugin removes or replaces an Arc<Supergraph> from its own state
  2. New requests can no longer obtain that owner
  3. Dropping the final owner publishes retirement
  4. Ordinary requests that already have a snapshot can finish
  5. Active subscriptions observe retirement and close with the schema-reload error
  6. The router's background cleanup task removes the retired runtime from its cache

A snapshot or runtime never delays owner retirement. Conversely, retiring the owner does not invalidate immutable schema data already held by an ordinary in-flight request (they can finish).

Router (internal)

RouterSupergraphRuntime

RouterSupergraphRuntime contains state that cannot be built by a plugin because it depends on router configuration or router-owned infrastructure:

  • The subgraph executor map
  • Operation-name forwarding configuration
  • Authorization metadata used by the router pipeline
  • Validation cache
  • Normalization cache
  • Query-plan cache
  • Demand-control runtime and formula cache

Runtime construction receives a SupergraphSnapshot, the router configuration, telemetry, and the shared callback subscriptions map. It retains neither the Supergraph owner nor the snapshot. Requests and streams carry the snapshot separately.

This split keeps the public Supergraph portable and prevents plugins from having to construct fake HiveRouterConfig, TelemetryContext, callback maps, or router state.

Why caches live on the runtime

My original design shared schema-aware caches keyed by a supergraph cache ID. The final implementation instead places every schema-derived cache directly on its RouterSupergraphRuntime.

This is simpler and gives the same isolation guarantee structurally:

  • Two distinct Supergraph instances cannot share validation, normalization, plan, or demand-control formula entries because they have different runtimes
  • Cache keys dont need a supergraph ID because the cache itself belongs to exactly one runtime
  • Retiring or evicting a runtime naturally retires its cache entries with it
  • Configured schema rotation does not globally invalidate unrelated plugin caches
  • Old configured caches remain available only to requests or streams still holding the old runtime

Note that the parse cache remains shared because parsing a GraphQL operation is schema-independent!

The process-local supergraph ID is still required for two purposes:

  • Looking up a plugin-selected runtime in the runtime cache
  • Partitioning in-flight request deduplication so identical operations against distinct supergraph instances never join the same execution

It's important to know that the ID is instance identity, not content identity! Two separately constructed supergraphs receive different IDs even if their SDL or consumer schemas are identical.

Configured and plugin-selected runtimes

Configured supergraph

SchemaState has one atomic configured slot containing:

  • The configured Arc<Supergraph> owner
  • Its SupergraphSnapshot
  • Its eagerly built Arc<RouterSupergraphRuntime>

These values are published together with ArcSwap. A request can therefore never observe a schema from one generation and a runtime from another.

The configured runtime does not participate in the bounded plugin runtime cache. The current configured value is pinned by the configured slot and is never evicted. It follows the same ownership and retirement rules as a plugin variant, but its owner is retained by SchemaState instead of a plugin.

Configured loading and rotation follow this order:

  1. Load and parse the new supergraph SDL
  2. Run the existing supergraph load hooks
  3. Construct the schema-only Supergraph
  4. Run end hooks that may replace it
  5. Build RouterSupergraphRuntime
  6. Atomically publish owner, snapshot, and runtime
  7. Drop the previous configured owner after the swap

If schema or runtime construction fails, the current configured value remains untouched. A successful swap retires only the previous configured generation.

Swapping no longer clears the complete shared schema caches or globally closes all subscriptions because dropping the old supergraph wil retire it and that will clean up its own cache and related subscriptions.

Plugin-selected supergraph

Plugin runtimes are built lazily because the router cannot know which variants a plugin will select. SchemaState uses a Mutex<VecDeque<...>> as a strict FIFO runtime cache:

  • Maximum size is 10
  • The same supergraph ID reuses the same runtime
  • Cache hits do not change insertion order
  • Runtime construction is serialized under the mutex, so concurrent first requests build once
  • Failed construction is not inserted
  • Selecting a broken plugin variant fails closed and never falls back to the configured supergraph
  • Inserting an eleventh live variant evicts the oldest cached runtime

FIFO eviction only removes the router's cached Arc<RouterSupergraphRuntime>. It does not drop the plugin's Arc<Supergraph>, publish retirement, or close subscriptions. If the plugin selects that still-live supergraph again, the router will rebuild its runtime.

Background runtime cleanup

A router-managed RuntimeCacheCleanupTask receives registrations when plugin runtimes enter the cache. Each registration includes the supergraph ID and a clone of its retirement token. The task waits for retirement and removes the matching runtime entry.

FIFO eviction sends a corresponding eviction message so the task can cancel and discard a waiter that no longer has a cached runtime to clean up. Registration is deduplicated by supergraph ID, including the case where a still-live supergraph is evicted and later inserted again.

This task has deliberately narrow authority:

  • Owner retirement can remove a runtime cache entry
  • FIFO eviction can remove a runtime cache entry
  • Runtime removal cannot retire a Supergraph
  • Runtime removal cannot stop an ordinary request or stream that already holds its own runtime Arc

The cache is still bounded if the cleanup task is unavailable. The task exists to release retired executors and schema caches earlier, not to provide the memory bound.

Configured runtimes do not need cleanup-task registration. Replacing the atomic configured slot immediately releases the slot's old runtime reference therefore dropping it. Requests and streams keep it alive only for as long as they still use it.

Eviction != retirement

Runtime eviction and supergraph retirement are deliberately separate lifecycle events.

The plugin runtime cache stores only:

supergraph ID -> Arc<RouterSupergraphRuntime>

It does not store the owning Arc<Supergraph> or a complete SelectedSupergraph.

Eviction removes only the cache's Arc<RouterSupergraphRuntime>. It does not drop the plugin's Arc<Supergraph>, cancel the supergraph's retirement token, make the supergraph unselectable, or close its subscriptions.

A running request or subscription holds its own SelectedSupergraph, which contains:

  • The SupergraphSnapshot
  • An Arc<RouterSupergraphRuntime>

Therefore, evicting the cached runtime does not invalidate work already using it. The subscription's runtime remains alive through its own Arc, and the subscription continues normally.

If the plugin still owns the Supergraph and selects it again after its runtime was evicted, the router builds and caches a new runtime. Both runtimes are derived from the same immutable supergraph and router configuration, so this temporary overlap is safe.

Subscriptions close with SUBSCRIPTION_SCHEMA_RELOAD only when the selected supergraph retires. Retirement occurs when the final owning Arc is dropped. Supergraph::drop cancels its retirement token, and subscription producers observing that token broadcast the reload error and stop. Existing subscriptions may continue using the previous runtime while new requests use the rebuilt runtime.

The background cleanup task may remove a cached runtime after its owner retires, but that removal is a consequence of retirement, not its cause. The retirement token closes subscriptions; cache cleanup only releases the router's cached runtime reference.

Request selection and consistency

SchemaState::select_supergraph applies one selection rule:

  1. Reuse a SelectedSupergraph already pinned to the request (selecting multiple times throught)
  2. Otherwise prefer a plugin-provided SupergraphSnapshot from request extensions (plugin set)
  3. Otherwise use the configured owner, snapshot, and runtime (configuration set)
  4. If neither exists, return no selection
  5. If a plugin runtime cannot be built, return the runtime error without falling back
    • Deliberate decision, if a plugin provided a supergraph but cant be built - it would be a security issue to fall back to the configured one...

SelectedSupergraph contains the exact snapshot and runtime pair. It is stored back into request extensions for both plugin and configured selections. This makes later users consume the same generation rather than reading the global configured slot again.

The selected pair is used by:

  • Validation and validation plugins
  • Progressive override state
  • GraphQL request and analysis coprocessors
  • Normalization
  • Variable coercion
  • Authorization
  • Query planning and query-plan plugins
  • Demand control
  • Introspection
  • Subgraph execution
  • Operation-name forwarding
  • Usage reporting
  • In-flight request deduplication
  • The GraphQL response coprocessor

The response coprocessor reads the pinned request selection. This matters during configured rotation because re-reading the current configured slot after execution could expose SDL from a newer generation than the one that produced the response.

Ordinary requests, deduplication, and subscriptions

Ordinary requests

An ordinary request keeps SelectedSupergraph alive through execution and response processing. If the owner retires during the request, the snapshot and runtime remain valid and the request completes normally.

This avoids aborting safe immutable work while still preventing future requests from selecting a removed owner.

In-flight request deduplication

The request fingerprint now includes the selected supergraph's instance ID instead of the consumer-schema checksum. Consumer schemas can be identical while routing supergraphs, subgraph endpoints, planner state, or ownership lifetimes differ.

This prevents requests for different supergraph instances from sharing an in-flight execution. Deduplicated subscription consumers still share one producer only when they selected the same supergraph instance and have the same remaining fingerprint inputs.

Subscription retirement

The subscription producer pump retains the complete SelectedSupergraph, not only its token. This keeps the selected snapshot and router runtime alive for the full stream lifetime, including after the HTTP handler has returned streaming headers.

The pump selects between the next upstream item and SupergraphSnapshot::retired(). Retirement broadcasts the existing error and stops the producer like before during schema reload.

Each closes only subscriptions selected from the retired owner. The previous global close_all_with_error call on configured reload is removed.

WebSockets

A valid WebSocket upgrade resolves and pins its SelectedSupergraph during the first message (not during the HTTP upgrade).

An upgrade without any selected or configured supergraph rejecting with HTTP 503 would be invisible to browsers because browsers dont provide insights to the WebSocket client about why the HTTP Upgrade failed.

We therefore instead accept the WebSocket connection and close it with a specific close code and message surfacing the error to the clients and giving insight into what happened.

If that supergraph later retires:

  • Existing subscription producers terminate through the retirement token
  • New operations on the connection are rejected as unavailable (explained above)
  • The connection cannot silently switch to a newly configured generation

This is the same lifetime rule as an ordinary request, extended to the WebSocket connection instead of one operation.

supergraph.source: plugin

The new configuration is:

supergraph:
  source: plugin

This mode means there is no configured supergraph:

  • No file, Hive, or storage loader is created
  • No supergraph polling task is registered
  • The configured slot remains empty
  • Plugins, telemetry, caches, callback handling, and the HTTP server still initialize normally
  • A GraphQL request without a plugin-selected supergraph returns NO_SUPERGRAPH_AVAILABLE with HTTP 503
  • A WebSocket connection without a plugin-selected supergraph is closed with No supergraph available yet
  • A selected supergraph whose runtime cannot be built returns an internal runtime error

Existing environment overrides for file and Hive sources continue to use the existing configuration override behavior. No plugin-source environment variable is added.

Health, readiness, and Prometheus

Health and readiness now pass through the plugin on_http_request chain and its on_end callbacks. This is required because readiness in plugin-only mode must allow the plugin to select a supergraph for that specific readiness request.

Probe behavior is:

  • Health reports process liveness and remains 200 unless a plugin explicitly changes or ends the request
  • Readiness resolves the request's plugin-selected supergraph first and the configured fallback second
  • Readiness is 200 only when the selected supergraph is not retired and has a usable runtime
  • Missing selection or runtime construction failure produces 503
  • Coprocessors do not run for health or readiness
  • Prometheus keeps bypassing both plugins and coprocessors

Readiness selection is request-local. A plugin selecting a supergraph for one readiness request does not publish it as a configured default.

Callback subscriptions

All configured and plugin-selected runtimes receive the same router-owned callback subscriptions map. The router also runs one heartbeat enforcer over that map.

Plugins never construct or own this infrastructure. Sharing the map preserves callback routing and heartbeat enforcement regardless of which supergraph a request selected.

Hook and error ownership

Supergraph load hooks continue to apply only to configured-source loading. They receive schema state without router-only executor state, and a plugin-selected Supergraph does not trigger configured reload hooks.

Error ownership follows the schema/runtime split:

  • SDL parsing and planner construction errors come from Supergraph construction
  • Subgraph executor and authorization runtime construction errors come from RouterSupergraphRuntime construction
  • Missing selection uses PipelineError::NoSupergraphAvailable
  • A plugin runtime build failure uses PipelineError::RouterSupergraphRuntimeError and is not cached

OnGraphQLValidationStartHookPayload::with_schema is removed because it cannot provide pipeline-wide consistency. Plugins should migrate to set_supergraph in on_http_request.

Examples

plugin_examples/replace_schema

This example has a normal configured file supergraph. The plugin builds and retains one additional Arc<Supergraph> during initialization.

Requests without the x-schema-variant: basic header use the configured default. Requests with that header select the plugin's stripped basic supergraph. The example demonstrates request-local override with a configured fallback, including validation and introspection behavior.

It also covers the fail-closed rule: selecting a variant whose router runtime cannot be constructed returns a runtime error instead of falling back to the configured schema.

plugin_examples/feature_flags

This example uses:

supergraph:
  source: plugin

There is no configured default. The plugin owns the base document and a map of Arc<Supergraph> variants. It normalizes the feature-flag header, constructs each variant once, retains it in the map, and selects it with set_supergraph.

Every GraphQL request and readiness request must receive a plugin selection. If the plugin intentionally skips selection, the router returns NO_SUPERGRAPH_AVAILABLE. The example demonstrates full plugin ownership and switching among multiple schemas without any configured fallback.

Together the examples cover both supported models: override a configured default, or make the plugin the only supergraph source.

Intentional tradeoffs

  • Runtime construction is serialized on cache misses. Misses should be rare, and this guarantees one construction without another single-flight abstraction
  • The plugin runtime cache uses strict FIFO with a fixed capacity of 10. Hits do not refresh order, keeping behavior deterministic
  • Failed runtime construction is retried on a later request. Negative caching is deferred until repeated deterministic failures are shown to be a real problem
  • Retirement is based on owner lifetime, not SDL equality. Rebuilding identical SDL creates a new lifecycle and a new deduplication identity
  • Ordinary in-flight requests are allowed to finish after retirement. Only long-lived subscription work is actively terminated
  • The configured runtime is not placed in the plugin FIFO cache. Its atomic slot already provides the required permanent pin and rotation boundary

TODOs

  • docs Hive Router's request selected supergraphs and other goodies docs#149
  • rename SchemaState because its confusing, or merge it with RouterSharedState for later times
  • rename new_supergraph_data in OnSupergraphLoadEndHookPayload to just new_supergraph because it points to Supergraph. or should it point to SupergraphData still?
  • (we shouldnt) should we expose the actual supergraph runtime error? I think it should be exposed unless masking is disabled (future)
  • subgraph url overrides should have sort of a label that will allow users to override with dynamic supergraphs think and decide about labeling in the future. for now, when the plugins are the only one that can dhynamically switch the supergraph - they have full control by managing their own map of the supergraphs, i.e. subgraph url overrides can happen directly in the plugin

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the router's schema state management to support request-specific supergraph variants, allowing plugins to select different supergraphs dynamically (e.g., for feature flags or schema overrides). It introduces a bounded FIFO cache for supergraph runtimes, moves validation, normalization, and planning caches to be supergraph-specific, and adds a background cleanup task for retired supergraphs. Feedback focuses on resolving a performance bottleneck in the feature flags plugin where a Mutex lock is held over heavy operations, replacing several .unwrap() calls on Mutex locks with .expect() to provide explanatory panic messages, and flattening deeply nested control flows in both the cleanup task and subscription chunk loops.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread plugin_examples/feature_flags/src/plugin.rs
Comment thread bin/router/src/schema_state.rs
Comment thread bin/router/src/schema_state.rs
Comment thread bin/router/src/schema_state.rs
Comment thread bin/router/src/schema_state.rs
Comment thread bin/router/src/pipeline/mod.rs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces request-scoped supergraph selection via plugins, splitting schema-derived state (public Supergraph + SupergraphSnapshot) from router-owned runtime state (RouterSupergraphRuntime). It adds a new supergraph.source: plugin mode (no configured loader) and ensures the selected supergraph is used consistently across validation, normalization, planning, execution, introspection, usage reporting, deduplication, and WebSockets.

Changes:

  • Add public Supergraph ownership + snapshot/retirement model in the executor crate, and allow plugins to select it via OnHttpRequestHookPayload::set_supergraph.
  • Refactor the router to build and cache per-supergraph RouterSupergraphRuntime (configured runtime eagerly, plugin-selected runtimes lazily via a bounded FIFO cache + cleanup task).
  • Update probes (readiness/health), WebSockets, and e2e tests to respect request-pinned supergraph selection and per-runtime cache isolation; add new plugin examples (replace_schema) and update feature_flags to plugin-only sourcing.

Reviewed changes

Copilot reviewed 40 out of 42 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
plugin_examples/replace_schema/supergraph.graphql Adds example supergraph SDL used by the replace-schema plugin example.
plugin_examples/replace_schema/src/test.rs Adds e2e tests verifying per-request supergraph override and fail-closed behavior.
plugin_examples/replace_schema/src/plugin.rs Implements a plugin that constructs and selects an alternate Arc<Supergraph> per request.
plugin_examples/replace_schema/src/main.rs Adds a runnable example binary registering the plugin.
plugin_examples/replace_schema/src/lib.rs Exposes plugin and tests for the new example crate.
plugin_examples/replace_schema/router.config.yaml Adds example router config using supergraph.source: file plus the replace-schema plugin.
plugin_examples/replace_schema/Cargo.toml Adds the new replace-schema-plugin-example crate.
plugin_examples/feature_flags/src/test.rs Expands tests for plugin-only supergraph selection and runtime build failure/no-selection paths.
plugin_examples/feature_flags/src/plugin.rs Migrates feature_flags example to select Arc<Supergraph> in on_http_request and cache variants in-plugin.
plugin_examples/feature_flags/router.config.yaml Switches the example to supergraph.source: plugin.
plugin_examples/Cargo.toml Registers the new replace_schema example crate in the plugin examples workspace.
plugin_examples/Cargo.lock Updates lockfile for added example crate and dependency graph changes.
lib/router-config/src/supergraph.rs Adds SupergraphSource::Plugin config variant and display name mapping.
lib/executor/src/plugins/hooks/on_supergraph_load.rs Refactors supergraph loading output into public Supergraph + snapshots + retirement token model.
lib/executor/src/plugins/hooks/on_http_request.rs Adds OnHttpRequestHookPayload::set_supergraph for plugin selection into request extensions.
lib/executor/src/plugins/hooks/on_graphql_validation.rs Removes with_schema and clarifies validation schema semantics.
lib/executor/Cargo.toml Adds tokio-util dependency for CancellationToken.
e2e/src/supergraph.rs Updates e2e to validate cache isolation across configured supergraph reloads (per-runtime caches).
Cargo.lock Updates workspace lockfile for new tokio-util usage.
bin/router/src/supergraph/mod.rs Returns a specific error for supergraph.source: plugin (no loader).
bin/router/src/supergraph/base.rs Adds NoLoaderForPluginSource load error variant.
bin/router/src/shared_state.rs Removes shared schema caches and keeps parse cache local to shared state.
bin/router/src/schema_state.rs Major refactor: adds RouterSupergraphRuntime, selection/pinning, bounded runtime cache, and cleanup task.
bin/router/src/plugins/plugins_service.rs Runs plugins for health/readiness but continues to bypass coprocessor; keeps Prometheus bypass.
bin/router/src/pipeline/websocket_server.rs Pins/validates supergraph on first WS message; rejects operations if no supergraph or retired selection.
bin/router/src/pipeline/validation/mod.rs Moves validation cache access to per-runtime cache keyed by selected supergraph.
bin/router/src/pipeline/query_plan.rs Moves plan cache access to per-runtime cache and uses snapshot planner for planning.
bin/router/src/pipeline/normalize.rs Refactors normalization to use snapshot + runtime caches instead of a shared schema cache.
bin/router/src/pipeline/mod.rs Uses request-selected supergraph throughout pipeline; updates dedupe fingerprint to use supergraph instance id.
bin/router/src/pipeline/execution.rs Switches execution to use selected runtime executors and snapshot metadata/schema.
bin/router/src/pipeline/error.rs Adds pipeline error variant for supergraph runtime build errors.
bin/router/src/pipeline/demand_control/runtime.rs Updates demand-control evaluation to use SupergraphSnapshot.
bin/router/src/pipeline/coerce_variables.rs Updates variable coercion to use SupergraphSnapshot.
bin/router/src/pipeline/authorization/mod.rs Re-exports AuthorizationMetadataExt and adjusts imports after refactor.
bin/router/src/lib.rs Wires cache-size observers to new shared/runtime cache layout and ensures response coprocessor uses pinned selection.
bin/router/src/http_utils/probes.rs Makes readiness request-aware (SchemaState::is_ready(&req)) so plugin selection can satisfy readiness.
bin/router/src/cache_state.rs Updates cache size observers to sum per-runtime caches and read parse cache from shared state.
.changeset/supergraph_source_plugin.md Changeset documenting supergraph.source: plugin.
.changeset/replace_the_schema_state_in_the_on_http_request_plugin_hook.md Changeset documenting set_supergraph + runtime split/caching behavior.
.changeset/health_readiness_plugin_hooks.md Changeset documenting plugin hook behavior for health/readiness.
.changeset/drop_with_schema_from_on_graphql_validation_plugin_hook.md Changeset documenting removal of with_schema and migration guidance.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugin_examples/feature_flags/src/plugin.rs
Comment thread plugin_examples/feature_flags/src/plugin.rs
Comment thread bin/router/src/pipeline/websocket_server.rs Outdated
Comment thread bin/router/src/pipeline/websocket_server.rs
Comment thread bin/router/src/pipeline/normalize.rs Outdated
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

🐋 This PR was built and pushed to the following Docker images:

Image Names: ghcr.io/graphql-hive/router

Platforms: linux/amd64,linux/arm64

Image Tags: ghcr.io/graphql-hive/router:pr-1284 ghcr.io/graphql-hive/router:sha-2a2f734

Docker metadata
{
"buildx.build.provenance/linux/amd64": {
  "builder": {
    "id": "https://github.com/graphql-hive/router/actions/runs/29856788774/attempts/1"
  },
  "buildType": "https://mobyproject.org/buildkit@v1",
  "materials": [
    {
      "uri": "pkg:docker/docker/dockerfile@1.22",
      "digest": {
        "sha256": "4a43a54dd1fedceb30ba47e76cfcf2b47304f4161c0caeac2db1c61804ea3c91"
      }
    },
    {
      "uri": "pkg:docker/gcr.io/distroless/cc-debian12@latest?platform=linux%2Famd64",
      "digest": {
        "sha256": "e8e7ee4b8b106d4c5fde9e422a321b2b8a2d5cca546c97adcce927f3e1d36e36"
      }
    }
  ],
  "invocation": {
    "configSource": {
      "entryPoint": "router.Dockerfile"
    },
    "parameters": {
      "frontend": "gateway.v0",
      "args": {
        "cmdline": "docker/dockerfile:1.22",
        "label:org.opencontainers.image.created": "2026-07-21T18:40:29.158Z",
        "label:org.opencontainers.image.description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
        "label:org.opencontainers.image.licenses": "MIT",
        "label:org.opencontainers.image.revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
        "label:org.opencontainers.image.source": "https://github.com/graphql-hive/router",
        "label:org.opencontainers.image.title": "router",
        "label:org.opencontainers.image.url": "https://github.com/graphql-hive/router",
        "label:org.opencontainers.image.vendor": "theguild",
        "label:org.opencontainers.image.version": "pr-1284",
        "source": "docker/dockerfile:1.22"
      },
      "locals": [
        {
          "name": "context"
        },
        {
          "name": "dockerfile"
        }
      ],
      "root": {
        "configSource": {
          "path": "router.Dockerfile"
        },
        "request": {
          "args": {
            "label:org.opencontainers.image.created": "2026-07-21T18:40:29.158Z",
            "label:org.opencontainers.image.description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
            "label:org.opencontainers.image.licenses": "MIT",
            "label:org.opencontainers.image.revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
            "label:org.opencontainers.image.source": "https://github.com/graphql-hive/router",
            "label:org.opencontainers.image.title": "router",
            "label:org.opencontainers.image.url": "https://github.com/graphql-hive/router",
            "label:org.opencontainers.image.vendor": "theguild",
            "label:org.opencontainers.image.version": "pr-1284",
            "vcs:localdir:context": ".",
            "vcs:localdir:dockerfile": "docker",
            "vcs:revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
            "vcs:source": "https://github.com/graphql-hive/router"
          }
        }
      },
      "compatibilityVersion": 30
    },
    "environment": {
      "github_actor": "enisdenjo",
      "github_actor_id": "11807600",
      "github_event_name": "pull_request",
      "github_event_payload": {
        "action": "synchronize",
        "after": "5b665bc8a4b61026ffb1bc05c721a516b475f343",
        "before": "d1d3845513e7b07401a41f74d256700c85f5988f",
        "enterprise": {
          "avatar_url": "https://avatars.githubusercontent.com/b/187753?v=4",
          "created_at": "2024-07-02T08:52:28Z",
          "description": "",
          "html_url": "https://github.com/enterprises/the-guild",
          "id": 187753,
          "name": "The Guild",
          "node_id": "E_kgDOAALdaQ",
          "slug": "the-guild",
          "updated_at": "2026-07-11T07:16:45Z",
          "website_url": "https://the-guild.dev/"
        },
        "number": 1284,
        "organization": {
          "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
          "description": "Schema registry, analytics and gateway for GraphQL federation and other GraphQL APIs.",
          "events_url": "https://api.github.com/orgs/graphql-hive/events",
          "hooks_url": "https://api.github.com/orgs/graphql-hive/hooks",
          "id": 182742256,
          "issues_url": "https://api.github.com/orgs/graphql-hive/issues",
          "login": "graphql-hive",
          "members_url": "https://api.github.com/orgs/graphql-hive/members{/member}",
          "node_id": "O_kgDOCuRs8A",
          "public_members_url": "https://api.github.com/orgs/graphql-hive/public_members{/member}",
          "repos_url": "https://api.github.com/orgs/graphql-hive/repos",
          "url": "https://api.github.com/orgs/graphql-hive"
        },
        "pull_request": {
          "_links": {
            "comments": {
              "href": "https://api.github.com/repos/graphql-hive/router/issues/1284/comments"
            },
            "commits": {
              "href": "https://api.github.com/repos/graphql-hive/router/pulls/1284/commits"
            },
            "html": {
              "href": "https://github.com/graphql-hive/router/pull/1284"
            },
            "issue": {
              "href": "https://api.github.com/repos/graphql-hive/router/issues/1284"
            },
            "review_comment": {
              "href": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}"
            },
            "review_comments": {
              "href": "https://api.github.com/repos/graphql-hive/router/pulls/1284/comments"
            },
            "self": {
              "href": "https://api.github.com/repos/graphql-hive/router/pulls/1284"
            },
            "statuses": {
              "href": "https://api.github.com/repos/graphql-hive/router/statuses/5b665bc8a4b61026ffb1bc05c721a516b475f343"
            }
          },
          "active_lock_reason": null,
          "additions": 2435,
          "assignee": null,
          "assignees": [],
          "author_association": "MEMBER",
          "auto_merge": null,
          "base": {
            "label": "graphql-hive:main",
            "ref": "main",
            "repo": {
              "allow_auto_merge": false,
              "allow_forking": true,
              "allow_merge_commit": false,
              "allow_rebase_merge": false,
              "allow_squash_merge": true,
              "allow_update_branch": true,
              "archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
              "archived": false,
              "assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
              "blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
              "branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
              "clone_url": "https://github.com/graphql-hive/router.git",
              "collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
              "comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
              "commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
              "compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
              "contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
              "contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
              "created_at": "2024-11-20T16:16:12Z",
              "default_branch": "main",
              "delete_branch_on_merge": true,
              "deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
              "description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
              "disabled": false,
              "downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
              "events_url": "https://api.github.com/repos/graphql-hive/router/events",
              "fork": false,
              "forks": 16,
              "forks_count": 16,
              "forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
              "full_name": "graphql-hive/router",
              "git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
              "git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
              "git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
              "git_url": "git://github.com/graphql-hive/router.git",
              "has_discussions": false,
              "has_downloads": false,
              "has_issues": true,
              "has_pages": false,
              "has_projects": false,
              "has_pull_requests": true,
              "has_wiki": false,
              "homepage": "https://the-guild.dev/graphql/hive/docs/router",
              "hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
              "html_url": "https://github.com/graphql-hive/router",
              "id": 891604244,
              "is_template": false,
              "issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
              "issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
              "issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
              "keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
              "labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
              "language": "Rust",
              "languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
              "license": {
                "key": "mit",
                "name": "MIT License",
                "node_id": "MDc6TGljZW5zZTEz",
                "spdx_id": "MIT",
                "url": "https://api.github.com/licenses/mit"
              },
              "merge_commit_message": "PR_TITLE",
              "merge_commit_title": "MERGE_MESSAGE",
              "merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
              "milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
              "mirror_url": null,
              "name": "router",
              "node_id": "R_kgDONSTNFA",
              "notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
              "open_issues": 62,
              "open_issues_count": 62,
              "owner": {
                "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
                "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
                "followers_url": "https://api.github.com/users/graphql-hive/followers",
                "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
                "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
                "gravatar_id": "",
                "html_url": "https://github.com/graphql-hive",
                "id": 182742256,
                "login": "graphql-hive",
                "node_id": "O_kgDOCuRs8A",
                "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
                "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
                "repos_url": "https://api.github.com/users/graphql-hive/repos",
                "site_admin": false,
                "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
                "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
                "type": "Organization",
                "url": "https://api.github.com/users/graphql-hive",
                "user_view_type": "public"
              },
              "private": false,
              "pull_request_creation_policy": "all",
              "pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
              "pushed_at": "2026-07-21T18:20:18Z",
              "releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
              "size": 10502,
              "squash_merge_commit_message": "PR_BODY",
              "squash_merge_commit_title": "PR_TITLE",
              "ssh_url": "git@github.com:graphql-hive/router.git",
              "stargazers_count": 94,
              "stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
              "statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
              "subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
              "subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
              "svn_url": "https://github.com/graphql-hive/router",
              "tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
              "teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
              "topics": [
                "apollo-federation",
                "federation",
                "federation-gateway",
                "graphql",
                "graphql-federation",
                "router"
              ],
              "trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
              "updated_at": "2026-07-21T08:44:41Z",
              "url": "https://api.github.com/repos/graphql-hive/router",
              "use_squash_pr_title_as_default": true,
              "visibility": "public",
              "watchers": 94,
              "watchers_count": 94,
              "web_commit_signoff_required": false
            },
            "sha": "a822fe117fb035d300bed69f28b140bf57efb446",
            "user": {
              "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
              "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
              "followers_url": "https://api.github.com/users/graphql-hive/followers",
              "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
              "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
              "gravatar_id": "",
              "html_url": "https://github.com/graphql-hive",
              "id": 182742256,
              "login": "graphql-hive",
              "node_id": "O_kgDOCuRs8A",
              "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
              "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
              "repos_url": "https://api.github.com/users/graphql-hive/repos",
              "site_admin": false,
              "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
              "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
              "type": "Organization",
              "url": "https://api.github.com/users/graphql-hive",
              "user_view_type": "public"
            }
          },
          "body": "Supersedes and therefore closes graphql-hive/router#1269, closes graphql-hive/router#1137, addresses graphql-hive/router#524 through a plugin\n\nAllow a plugin to select a supergraph document during on_http_request and have it hold for each HTTP request or WebSocket upgrade. The selected supergraph is used consistently across validation, introspection, normalization, planning, authorization, demand control, execution, coprocessors, usage reporting, subscriptions, and request deduplication.\n\nThe implementation separates two kinds of state that were previously bundled together:\n\n* `Supergraph` (executor) owns schema-derived data that a plugin can safely construct and retain\n* `RouterSupergraphRuntime` (router) owns router-specific state derived from both that supergraph and the router configuration\n\nConfigured supergraphs still build their runtime eagerly before publication. Plugin-selected supergraphs build their runtime lazily on first use and reuse it through a bounded router-owned cache.\n\nAlso adding `supergraph.source: plugin`, where there is deliberately no configured fallback. A plugin must select a supergraph for each request that needs one.\n\n# Executor (public)\n\n## `Supergraph`\n\n`Supergraph` is the public owner handle. Plugins construct it with either:\n\n```rust\nSupergraph::from_sdl(...)\nSupergraph::from_document(...)\n```\n\nConstruction only accepts query-planner options. It does not accept router configuration, telemetry, callback subscription maps, HTTP clients, or other router-owned state.\n\nThe schema-derived data includes:\n\n* The parsed supergraph document\n* The public consumer schema and its SDL\n* The query planner and planner supergraph state\n* Schema metadata\n* A process-local cache identity\n\nPlugins retain an `Arc<Supergraph>` for as long as that variant should remain selectable. `OnHttpRequestHookPayload::set_supergraph` immediately takes a `SupergraphSnapshot` and stores that snapshot in request extensions. The router does not retain the plugin's `Arc<Supergraph>` owner.\n\nThis distinction is important. A request snapshot keeps the immutable schema data alive, but it does not keep the owner alive. The plugin remains the authority over whether new requests can select a variant!\n\n## Retirement\n\n`Supergraph` owns a `CancellationToken`. `SupergraphSnapshot` receives a clone of that token. When the last `Arc<Supergraph>` is dropped, `Supergraph::drop` cancels the token.\n\nThis gives retirement the required ownership semantics without callbacks from the executor crate into the router:\n\n1. The plugin removes or replaces an `Arc<Supergraph>` from its own state\n2. New requests can no longer obtain that owner\n3. Dropping the final owner publishes retirement\n4. Ordinary requests that already have a snapshot can finish\n5. Active subscriptions observe retirement and close with the schema-reload error\n6. The router's background cleanup task removes the retired runtime from its cache\n\nA snapshot or runtime never delays owner retirement. Conversely, retiring the owner does not invalidate immutable schema data already held by an ordinary in-flight request (they can finish).\n\n# Router (internal)\n\n## `RouterSupergraphRuntime`\n\n`RouterSupergraphRuntime` contains state that cannot be built by a plugin because it depends on router configuration or router-owned infrastructure:\n\n* The subgraph executor map\n* Operation-name forwarding configuration\n* Authorization metadata used by the router pipeline\n* Validation cache\n* Normalization cache\n* Query-plan cache\n* Demand-control runtime and formula cache\n\nRuntime construction receives a `SupergraphSnapshot`, the router configuration, telemetry, and the shared callback subscriptions map. It retains neither the `Supergraph` owner nor the snapshot. Requests and streams carry the snapshot separately.\n\nThis split keeps the public `Supergraph` portable and prevents plugins from having to construct fake `HiveRouterConfig`, `TelemetryContext`, callback maps, or router state.\n\n## Why caches live on the runtime\n\nMy original design shared schema-aware caches keyed by a supergraph cache ID. The final implementation instead places every schema-derived cache directly on its `RouterSupergraphRuntime`.\n\nThis is simpler and gives the same isolation guarantee structurally:\n\n* Two distinct `Supergraph` instances cannot share validation, normalization, plan, or demand-control formula entries because they have different runtimes\n* Cache keys dont need a supergraph ID because the cache itself belongs to exactly one runtime\n* Retiring or evicting a runtime naturally retires its cache entries with it\n* Configured schema rotation does not globally invalidate unrelated plugin caches\n* Old configured caches remain available only to requests or streams still holding the old runtime\n\nNote that the parse cache remains shared because parsing a GraphQL operation is schema-independent!\n\nThe process-local supergraph ID is still required for two purposes:\n\n* Looking up a plugin-selected runtime in the runtime cache\n* Partitioning in-flight request deduplication so identical operations against distinct supergraph instances never join the same execution\n\nIt's important to know that the ID is instance identity, not content identity! Two separately constructed supergraphs receive different IDs even if their SDL or consumer schemas are identical.\n\n# Configured and plugin-selected runtimes\n\n## Configured supergraph\n\n`SchemaState` has one atomic configured slot containing:\n\n* The configured `Arc<Supergraph>` owner\n* Its `SupergraphSnapshot`\n* Its eagerly built `Arc<RouterSupergraphRuntime>`\n\nThese values are published together with `ArcSwap`. A request can therefore never observe a schema from one generation and a runtime from another.\n\nThe configured runtime does not participate in the bounded plugin runtime cache. The current configured value is pinned by the configured slot and is never evicted. It follows the same ownership and retirement rules as a plugin variant, but its owner is retained by `SchemaState` instead of a plugin.\n\nConfigured loading and rotation follow this order:\n\n1. Load and parse the new supergraph SDL\n2. Run the existing supergraph load hooks\n3. Construct the schema-only `Supergraph`\n4. Run end hooks that may replace it\n5. Build `RouterSupergraphRuntime`\n6. Atomically publish owner, snapshot, and runtime\n7. Drop the previous configured owner after the swap\n\nIf schema or runtime construction fails, the current configured value remains untouched. A successful swap retires only the previous configured generation.\n\nSwapping no longer clears the complete shared schema caches or globally closes all subscriptions because dropping the old supergraph wil retire it and that will clean up its own cache and related subscriptions.\n\n## Plugin-selected supergraph\n\nPlugin runtimes are built lazily because the router cannot know which variants a plugin will select. `SchemaState` uses a `Mutex<VecDeque<...>>` as a strict FIFO runtime cache:\n\n* Maximum size is 10\n* The same supergraph ID reuses the same runtime\n* Cache hits do not change insertion order\n* Runtime construction is serialized under the mutex, so concurrent first requests build once\n* Failed construction is not inserted\n* Selecting a broken plugin variant fails closed and never falls back to the configured supergraph\n* Inserting an eleventh live variant evicts the oldest cached runtime\n\nFIFO eviction only removes the router's cached `Arc<RouterSupergraphRuntime>`. It does not drop the plugin's `Arc<Supergraph>`, publish retirement, or close subscriptions. If the plugin selects that still-live supergraph again, the router will rebuild its runtime.\n\n# Background runtime cleanup\n\nA router-managed `RuntimeCacheCleanupTask` receives registrations when plugin runtimes enter the cache. Each registration includes the supergraph ID and a clone of its retirement token. The task waits for retirement and removes the matching runtime entry.\n\nFIFO eviction sends a corresponding eviction message so the task can cancel and discard a waiter that no longer has a cached runtime to clean up. Registration is deduplicated by supergraph ID, including the case where a still-live supergraph is evicted and later inserted again.\n\nThis task has deliberately narrow authority:\n\n* Owner retirement can remove a runtime cache entry\n* FIFO eviction can remove a runtime cache entry\n* Runtime removal cannot retire a `Supergraph`\n* Runtime removal cannot stop an ordinary request or stream that already holds its own runtime `Arc`\n\nThe cache is still bounded if the cleanup task is unavailable. The task exists to release retired executors and schema caches earlier, not to provide the memory bound.\n\nConfigured runtimes do not need cleanup-task registration. Replacing the atomic configured slot immediately releases the slot's old runtime reference therefore dropping it. Requests and streams keep it alive only for as long as they still use it.\n\n## Eviction != retirement\n\nRuntime eviction and supergraph retirement are deliberately separate lifecycle events.\n\nThe plugin runtime cache stores only:\n\n```\nsupergraph ID -> Arc<RouterSupergraphRuntime>\n```\n\nIt does not store the owning `Arc<Supergraph>` or a complete `SelectedSupergraph`.\n\nEviction removes only the cache's `Arc<RouterSupergraphRuntime>`. It does not drop the plugin's `Arc<Supergraph>`, cancel the supergraph's retirement token, make the supergraph unselectable, or close its subscriptions.\n\nA running request or subscription holds its own SelectedSupergraph, which contains:\n\n* The `SupergraphSnapshot`\n* An `Arc<RouterSupergraphRuntime>`\n\nTherefore, evicting the cached runtime does not invalidate work already using it. The subscription's runtime remains alive through its own Arc, and the subscription continues normally.\n\nIf the plugin still owns the Supergraph and selects it again after its runtime was evicted, the router builds and caches a new runtime. Both runtimes are derived from the same immutable supergraph and router configuration, so this temporary overlap is safe.\n\nSubscriptions close with `SUBSCRIPTION_SCHEMA_RELOAD` **only** when the selected supergraph retires. Retirement occurs when the final owning Arc is dropped. `Supergraph::drop` cancels its retirement token, and subscription producers observing that token broadcast the reload error and stop. Existing subscriptions may continue using the previous runtime while new requests use the rebuilt runtime.\n\nThe background cleanup task may remove a cached runtime after its owner retires, but that removal is a consequence of retirement, not its cause. The retirement token closes subscriptions; cache cleanup only releases the router's cached runtime reference.\n\n# Request selection and consistency\n\n`SchemaState::select_supergraph` applies one selection rule:\n\n1. Reuse a `SelectedSupergraph` already pinned to the request (selecting multiple times throught)\n2. Otherwise prefer a plugin-provided `SupergraphSnapshot` from request extensions (plugin set)\n3. Otherwise use the configured owner, snapshot, and runtime (configuration set)\n4. If neither exists, return no selection\n5. If a plugin runtime cannot be built, return the runtime error without falling back\n   * Deliberate decision, if a plugin provided a supergraph but cant be built - it would be a security issue to fall back to the configured one...\n\n`SelectedSupergraph` contains the exact snapshot and runtime pair. It is stored back into request extensions for both plugin and configured selections. This makes later users consume the same generation rather than reading the global configured slot again.\n\nThe selected pair is used by:\n\n* Validation and validation plugins\n* Progressive override state\n* GraphQL request and analysis coprocessors\n* Normalization\n* Variable coercion\n* Authorization\n* Query planning and query-plan plugins\n* Demand control\n* Introspection\n* Subgraph execution\n* Operation-name forwarding\n* Usage reporting\n* In-flight request deduplication\n* The GraphQL response coprocessor\n\nThe response coprocessor reads the pinned request selection. This matters during configured rotation because re-reading the current configured slot after execution could expose SDL from a newer generation than the one that produced the response.\n\n# Ordinary requests, deduplication, and subscriptions\n\n## Ordinary requests\n\nAn ordinary request keeps `SelectedSupergraph` alive through execution and response processing. If the owner retires during the request, the snapshot and runtime remain valid and the request completes normally.\n\nThis avoids aborting safe immutable work while still preventing future requests from selecting a removed owner.\n\n## In-flight request deduplication\n\nThe request fingerprint now includes the selected supergraph's instance ID instead of the consumer-schema checksum. Consumer schemas can be identical while routing supergraphs, subgraph endpoints, planner state, or ownership lifetimes differ.\n\nThis prevents requests for different supergraph instances from sharing an in-flight execution. Deduplicated subscription consumers still share one producer only when they selected the same supergraph instance and have the same remaining fingerprint inputs.\n\n## Subscription retirement\n\nThe subscription producer pump retains the complete `SelectedSupergraph`, not only its token. This keeps the selected snapshot and router runtime alive for the full stream lifetime, including after the HTTP handler has returned streaming headers.\n\nThe pump selects between the next upstream item and `SupergraphSnapshot::retired()`. Retirement broadcasts the existing error and stops the producer like before during schema reload.\n\nEach closes only subscriptions selected from the retired owner. The previous global `close_all_with_error` call on configured reload is removed.\n\n## WebSockets\n\nA valid WebSocket upgrade resolves and pins its `SelectedSupergraph` during the first message (not during the HTTP upgrade).\n\nAn upgrade without any selected or configured supergraph rejecting with HTTP 503 would be invisible to browsers because browsers dont provide insights to the WebSocket client about why the HTTP Upgrade failed.\n\nWe therefore instead accept the WebSocket connection and close it with a specific close code and message surfacing the error to the clients and giving insight into what happened.\n\nIf that supergraph later retires:\n\n* Existing subscription producers terminate through the retirement token\n* New operations on the connection are rejected as unavailable (explained above)\n* The connection cannot silently switch to a newly configured generation\n\nThis is the same lifetime rule as an ordinary request, extended to the WebSocket connection instead of one operation.\n\n# `supergraph.source: plugin`\n\nThe new configuration is:\n\n```yaml\nsupergraph:\n  source: plugin\n```\n\nThis mode means there is no configured supergraph:\n\n* No file, Hive, or storage loader is created\n* No supergraph polling task is registered\n* The configured slot remains empty\n* Plugins, telemetry, caches, callback handling, and the HTTP server still initialize normally\n* A GraphQL request without a plugin-selected supergraph returns `NO_SUPERGRAPH_AVAILABLE` with HTTP 503\n* A WebSocket connection without a plugin-selected supergraph is closed with `No supergraph available yet`\n* A selected supergraph whose runtime cannot be built returns an internal runtime error\n\nExisting environment overrides for file and Hive sources continue to use the existing configuration override behavior. No plugin-source environment variable is added.\n\n# Health, readiness, and Prometheus\n\nHealth and readiness now pass through the plugin `on_http_request` chain and its `on_end` callbacks. This is required because readiness in plugin-only mode must allow the plugin to select a supergraph for that specific readiness request.\n\nProbe behavior is:\n\n* Health reports process liveness and remains 200 unless a plugin explicitly changes or ends the request\n* Readiness resolves the request's plugin-selected supergraph first and the configured fallback second\n* Readiness is 200 only when the selected supergraph is not retired and has a usable runtime\n* Missing selection or runtime construction failure produces 503\n* Coprocessors do not run for health or readiness\n* Prometheus keeps bypassing both plugins and coprocessors\n\nReadiness selection is request-local. A plugin selecting a supergraph for one readiness request does not publish it as a configured default.\n\n# Callback subscriptions\n\nAll configured and plugin-selected runtimes receive the same router-owned callback subscriptions map. The router also runs one heartbeat enforcer over that map.\n\nPlugins never construct or own this infrastructure. Sharing the map preserves callback routing and heartbeat enforcement regardless of which supergraph a request selected.\n\n# Hook and error ownership\n\nSupergraph load hooks continue to apply only to configured-source loading. They receive schema state without router-only executor state, and a plugin-selected `Supergraph` does not trigger configured reload hooks.\n\nError ownership follows the schema/runtime split:\n\n* SDL parsing and planner construction errors come from `Supergraph` construction\n* Subgraph executor and authorization runtime construction errors come from `RouterSupergraphRuntime` construction\n* Missing selection uses `PipelineError::NoSupergraphAvailable`\n* A plugin runtime build failure uses `PipelineError::RouterSupergraphRuntimeError` and is not cached\n\n`OnGraphQLValidationStartHookPayload::with_schema` is removed because it cannot provide pipeline-wide consistency. Plugins should migrate to `set_supergraph` in `on_http_request`.\n\n# Examples\n\n## `plugin_examples/replace_schema`\n\nThis example has a normal configured file supergraph. The plugin builds and retains one additional `Arc<Supergraph>` during initialization.\n\nRequests without the `x-schema-variant: basic` header use the configured default. Requests with that header select the plugin's stripped `basic` supergraph. The example demonstrates request-local override with a configured fallback, including validation and introspection behavior.\n\nIt also covers the fail-closed rule: selecting a variant whose router runtime cannot be constructed returns a runtime error instead of falling back to the configured schema.\n\n## `plugin_examples/feature_flags`\n\nThis example uses:\n\n```yaml\nsupergraph:\n  source: plugin\n```\n\nThere is no configured default. The plugin owns the base document and a map of `Arc<Supergraph>` variants. It normalizes the feature-flag header, constructs each variant once, retains it in the map, and selects it with `set_supergraph`.\n\nEvery GraphQL request and readiness request must receive a plugin selection. If the plugin intentionally skips selection, the router returns `NO_SUPERGRAPH_AVAILABLE`. The example demonstrates full plugin ownership and switching among multiple schemas without any configured fallback.\n\nTogether the examples cover both supported models: override a configured default, or make the plugin the only supergraph source.\n\n# Intentional tradeoffs\n\n* Runtime construction is serialized on cache misses. Misses should be rare, and this guarantees one construction without another single-flight abstraction\n* The plugin runtime cache uses strict FIFO with a fixed capacity of 10. Hits do not refresh order, keeping behavior deterministic\n* Failed runtime construction is retried on a later request. Negative caching is deferred until repeated deterministic failures are shown to be a real problem\n* Retirement is based on owner lifetime, not SDL equality. Rebuilding identical SDL creates a new lifecycle and a new deduplication identity\n* Ordinary in-flight requests are allowed to finish after retirement. Only long-lived subscription work is actively terminated\n* The configured runtime is not placed in the plugin FIFO cache. Its atomic slot already provides the required permanent pin and rotation boundary\n\n# TODOs\n\n- [ ] docs\n- [ ] ~~rename ~~`~~SchemaState~~`~~ because its confusing, or merge it with ~~`~~RouterSharedState~~` for later times\n- [X] rename `new_supergraph_data` in `OnSupergraphLoadEndHookPayload` to just `new_supergraph` because it points to `Supergraph`. or should it point to `SupergraphData` still?\n- [X] (we shouldnt) should we expose the actual supergraph runtime error? I think it should be exposed unless masking is disabled (future)\n- [ ] ~~subgraph url overrides should have sort of a label that will allow users to override with dynamic supergraphs~~ think and decide about labeling in the future. for now, when the plugins are the only one that can dhynamically switch the supergraph - they have full control by managing their own map of the supergraphs, i.e. subgraph url overrides can happen directly in the plugin",
          "changed_files": 43,
          "closed_at": null,
          "comments": 3,
          "comments_url": "https://api.github.com/repos/graphql-hive/router/issues/1284/comments",
          "commits": 42,
          "commits_url": "https://api.github.com/repos/graphql-hive/router/pulls/1284/commits",
          "created_at": "2026-07-15T22:43:28Z",
          "deletions": 623,
          "diff_url": "https://github.com/graphql-hive/router/pull/1284.diff",
          "draft": false,
          "head": {
            "label": "graphql-hive:super-replace-schemastate",
            "ref": "super-replace-schemastate",
            "repo": {
              "allow_auto_merge": false,
              "allow_forking": true,
              "allow_merge_commit": false,
              "allow_rebase_merge": false,
              "allow_squash_merge": true,
              "allow_update_branch": true,
              "archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
              "archived": false,
              "assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
              "blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
              "branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
              "clone_url": "https://github.com/graphql-hive/router.git",
              "collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
              "comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
              "commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
              "compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
              "contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
              "contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
              "created_at": "2024-11-20T16:16:12Z",
              "default_branch": "main",
              "delete_branch_on_merge": true,
              "deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
              "description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
              "disabled": false,
              "downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
              "events_url": "https://api.github.com/repos/graphql-hive/router/events",
              "fork": false,
              "forks": 16,
              "forks_count": 16,
              "forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
              "full_name": "graphql-hive/router",
              "git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
              "git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
              "git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
              "git_url": "git://github.com/graphql-hive/router.git",
              "has_discussions": false,
              "has_downloads": false,
              "has_issues": true,
              "has_pages": false,
              "has_projects": false,
              "has_pull_requests": true,
              "has_wiki": false,
              "homepage": "https://the-guild.dev/graphql/hive/docs/router",
              "hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
              "html_url": "https://github.com/graphql-hive/router",
              "id": 891604244,
              "is_template": false,
              "issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
              "issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
              "issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
              "keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
              "labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
              "language": "Rust",
              "languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
              "license": {
                "key": "mit",
                "name": "MIT License",
                "node_id": "MDc6TGljZW5zZTEz",
                "spdx_id": "MIT",
                "url": "https://api.github.com/licenses/mit"
              },
              "merge_commit_message": "PR_TITLE",
              "merge_commit_title": "MERGE_MESSAGE",
              "merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
              "milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
              "mirror_url": null,
              "name": "router",
              "node_id": "R_kgDONSTNFA",
              "notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
              "open_issues": 62,
              "open_issues_count": 62,
              "owner": {
                "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
                "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
                "followers_url": "https://api.github.com/users/graphql-hive/followers",
                "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
                "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
                "gravatar_id": "",
                "html_url": "https://github.com/graphql-hive",
                "id": 182742256,
                "login": "graphql-hive",
                "node_id": "O_kgDOCuRs8A",
                "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
                "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
                "repos_url": "https://api.github.com/users/graphql-hive/repos",
                "site_admin": false,
                "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
                "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
                "type": "Organization",
                "url": "https://api.github.com/users/graphql-hive",
                "user_view_type": "public"
              },
              "private": false,
              "pull_request_creation_policy": "all",
              "pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
              "pushed_at": "2026-07-21T18:20:18Z",
              "releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
              "size": 10502,
              "squash_merge_commit_message": "PR_BODY",
              "squash_merge_commit_title": "PR_TITLE",
              "ssh_url": "git@github.com:graphql-hive/router.git",
              "stargazers_count": 94,
              "stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
              "statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
              "subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
              "subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
              "svn_url": "https://github.com/graphql-hive/router",
              "tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
              "teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
              "topics": [
                "apollo-federation",
                "federation",
                "federation-gateway",
                "graphql",
                "graphql-federation",
                "router"
              ],
              "trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
              "updated_at": "2026-07-21T08:44:41Z",
              "url": "https://api.github.com/repos/graphql-hive/router",
              "use_squash_pr_title_as_default": true,
              "visibility": "public",
              "watchers": 94,
              "watchers_count": 94,
              "web_commit_signoff_required": false
            },
            "sha": "5b665bc8a4b61026ffb1bc05c721a516b475f343",
            "user": {
              "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
              "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
              "followers_url": "https://api.github.com/users/graphql-hive/followers",
              "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
              "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
              "gravatar_id": "",
              "html_url": "https://github.com/graphql-hive",
              "id": 182742256,
              "login": "graphql-hive",
              "node_id": "O_kgDOCuRs8A",
              "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
              "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
              "repos_url": "https://api.github.com/users/graphql-hive/repos",
              "site_admin": false,
              "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
              "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
              "type": "Organization",
              "url": "https://api.github.com/users/graphql-hive",
              "user_view_type": "public"
            }
          },
          "html_url": "https://github.com/graphql-hive/router/pull/1284",
          "id": 4064208307,
          "issue_url": "https://api.github.com/repos/graphql-hive/router/issues/1284",
          "labels": [],
          "locked": false,
          "maintainer_can_modify": false,
          "merge_commit_sha": "8670aa63c5afc721ef7464959d76115224dedb03",
          "mergeable": null,
          "mergeable_state": "unknown",
          "merged": false,
          "merged_at": null,
          "merged_by": null,
          "milestone": null,
          "node_id": "PR_kwDONSTNFM7yPuWz",
          "number": 1284,
          "patch_url": "https://github.com/graphql-hive/router/pull/1284.patch",
          "rebaseable": null,
          "requested_reviewers": [
            {
              "avatar_url": "https://avatars.githubusercontent.com/u/8167190?v=4",
              "events_url": "https://api.github.com/users/kamilkisiela/events{/privacy}",
              "followers_url": "https://api.github.com/users/kamilkisiela/followers",
              "following_url": "https://api.github.com/users/kamilkisiela/following{/other_user}",
              "gists_url": "https://api.github.com/users/kamilkisiela/gists{/gist_id}",
              "gravatar_id": "",
              "html_url": "https://github.com/kamilkisiela",
              "id": 8167190,
              "login": "kamilkisiela",
              "node_id": "MDQ6VXNlcjgxNjcxOTA=",
              "organizations_url": "https://api.github.com/users/kamilkisiela/orgs",
              "received_events_url": "https://api.github.com/users/kamilkisiela/received_events",
              "repos_url": "https://api.github.com/users/kamilkisiela/repos",
              "site_admin": false,
              "starred_url": "https://api.github.com/users/kamilkisiela/starred{/owner}{/repo}",
              "subscriptions_url": "https://api.github.com/users/kamilkisiela/subscriptions",
              "type": "User",
              "url": "https://api.github.com/users/kamilkisiela",
              "user_view_type": "public"
            }
          ],
          "requested_teams": [],
          "review_comment_url": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}",
          "review_comments": 21,
          "review_comments_url": "https://api.github.com/repos/graphql-hive/router/pulls/1284/comments",
          "state": "open",
          "statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/5b665bc8a4b61026ffb1bc05c721a516b475f343",
          "title": "feat(router): request selected supergraphs",
          "updated_at": "2026-07-21T18:20:20Z",
          "url": "https://api.github.com/repos/graphql-hive/router/pulls/1284",
          "user": {
            "avatar_url": "https://avatars.githubusercontent.com/u/11807600?v=4",
            "events_url": "https://api.github.com/users/enisdenjo/events{/privacy}",
            "followers_url": "https://api.github.com/users/enisdenjo/followers",
            "following_url": "https://api.github.com/users/enisdenjo/following{/other_user}",
            "gists_url": "https://api.github.com/users/enisdenjo/gists{/gist_id}",
            "gravatar_id": "",
            "html_url": "https://github.com/enisdenjo",
            "id": 11807600,
            "login": "enisdenjo",
            "node_id": "MDQ6VXNlcjExODA3NjAw",
            "organizations_url": "https://api.github.com/users/enisdenjo/orgs",
            "received_events_url": "https://api.github.com/users/enisdenjo/received_events",
            "repos_url": "https://api.github.com/users/enisdenjo/repos",
            "site_admin": false,
            "starred_url": "https://api.github.com/users/enisdenjo/starred{/owner}{/repo}",
            "subscriptions_url": "https://api.github.com/users/enisdenjo/subscriptions",
            "type": "User",
            "url": "https://api.github.com/users/enisdenjo",
            "user_view_type": "public"
          }
        },
        "repository": {
          "allow_forking": true,
          "archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
          "archived": false,
          "assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
          "blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
          "branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
          "clone_url": "https://github.com/graphql-hive/router.git",
          "collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
          "comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
          "commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
          "compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
          "contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
          "contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
          "created_at": "2024-11-20T16:16:12Z",
          "custom_properties": {
            "vanta_production_branch_name": "main"
          },
          "default_branch": "main",
          "deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
          "description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
          "disabled": false,
          "downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
          "events_url": "https://api.github.com/repos/graphql-hive/router/events",
          "fork": false,
          "forks": 16,
          "forks_count": 16,
          "forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
          "full_name": "graphql-hive/router",
          "git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
          "git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
          "git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
          "git_url": "git://github.com/graphql-hive/router.git",
          "has_discussions": false,
          "has_downloads": false,
          "has_issues": true,
          "has_pages": false,
          "has_projects": false,
          "has_pull_requests": true,
          "has_wiki": false,
          "homepage": "https://the-guild.dev/graphql/hive/docs/router",
          "hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
          "html_url": "https://github.com/graphql-hive/router",
          "id": 891604244,
          "is_template": false,
          "issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
          "issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
          "issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
          "keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
          "labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
          "language": "Rust",
          "languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
          "license": {
            "key": "mit",
            "name": "MIT License",
            "node_id": "MDc6TGljZW5zZTEz",
            "spdx_id": "MIT",
            "url": "https://api.github.com/licenses/mit"
          },
          "merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
          "milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
          "mirror_url": null,
          "name": "router",
          "node_id": "R_kgDONSTNFA",
          "notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
          "open_issues": 62,
          "open_issues_count": 62,
          "owner": {
            "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
            "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
            "followers_url": "https://api.github.com/users/graphql-hive/followers",
            "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
            "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
            "gravatar_id": "",
            "html_url": "https://github.com/graphql-hive",
            "id": 182742256,
            "login": "graphql-hive",
            "node_id": "O_kgDOCuRs8A",
            "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
            "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
            "repos_url": "https://api.github.com/users/graphql-hive/repos",
            "site_admin": false,
            "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
            "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
            "type": "Organization",
            "url": "https://api.github.com/users/graphql-hive",
            "user_view_type": "public"
          },
          "private": false,
          "pull_request_creation_policy": "all",
          "pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
          "pushed_at": "2026-07-21T18:20:18Z",
          "releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
          "size": 10502,
          "ssh_url": "git@github.com:graphql-hive/router.git",
          "stargazers_count": 94,
          "stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
          "statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
          "subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
          "subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
          "svn_url": "https://github.com/graphql-hive/router",
          "tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
          "teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
          "topics": [
            "apollo-federation",
            "federation",
            "federation-gateway",
            "graphql",
            "graphql-federation",
            "router"
          ],
          "trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
          "updated_at": "2026-07-21T08:44:41Z",
          "url": "https://api.github.com/repos/graphql-hive/router",
          "visibility": "public",
          "watchers": 94,
          "watchers_count": 94,
          "web_commit_signoff_required": false
        },
        "sender": {
          "avatar_url": "https://avatars.githubusercontent.com/u/11807600?v=4",
          "events_url": "https://api.github.com/users/enisdenjo/events{/privacy}",
          "followers_url": "https://api.github.com/users/enisdenjo/followers",
          "following_url": "https://api.github.com/users/enisdenjo/following{/other_user}",
          "gists_url": "https://api.github.com/users/enisdenjo/gists{/gist_id}",
          "gravatar_id": "",
          "html_url": "https://github.com/enisdenjo",
          "id": 11807600,
          "login": "enisdenjo",
          "node_id": "MDQ6VXNlcjExODA3NjAw",
          "organizations_url": "https://api.github.com/users/enisdenjo/orgs",
          "received_events_url": "https://api.github.com/users/enisdenjo/received_events",
          "repos_url": "https://api.github.com/users/enisdenjo/repos",
          "site_admin": false,
          "starred_url": "https://api.github.com/users/enisdenjo/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/enisdenjo/subscriptions",
          "type": "User",
          "url": "https://api.github.com/users/enisdenjo",
          "user_view_type": "public"
        }
      },
      "github_job": "docker",
      "github_ref": "refs/pull/1284/merge",
      "github_ref_name": "1284/merge",
      "github_ref_protected": "false",
      "github_ref_type": "branch",
      "github_repository": "graphql-hive/router",
      "github_repository_id": "891604244",
      "github_repository_owner": "graphql-hive",
      "github_repository_owner_id": "182742256",
      "github_run_attempt": "1",
      "github_run_id": "29856788774",
      "github_run_number": "4514",
      "github_runner_arch": "X64",
      "github_runner_environment": "github-hosted",
      "github_runner_image_os": "ubuntu24",
      "github_runner_image_version": "20260714.240.1",
      "github_runner_name": "GitHub Actions 1000922431",
      "github_runner_os": "Linux",
      "github_runner_tracking_id": "github_73f16072-67a6-4c6d-91cb-88ad758944e9",
      "github_server_url": "https://github.com",
      "github_triggering_actor": "enisdenjo",
      "github_workflow": "build-router",
      "github_workflow_ref": "graphql-hive/router/.github/workflows/build-router.yaml@refs/pull/1284/merge",
      "github_workflow_sha": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
      "platform": "linux/amd64"
    }
  }
},
"buildx.build.provenance/linux/arm64": {
  "builder": {
    "id": "https://github.com/graphql-hive/router/actions/runs/29856788774/attempts/1"
  },
  "buildType": "https://mobyproject.org/buildkit@v1",
  "materials": [
    {
      "uri": "pkg:docker/docker/dockerfile@1.22",
      "digest": {
        "sha256": "4a43a54dd1fedceb30ba47e76cfcf2b47304f4161c0caeac2db1c61804ea3c91"
      }
    },
    {
      "uri": "pkg:docker/gcr.io/distroless/cc-debian12@latest?platform=linux%2Farm64",
      "digest": {
        "sha256": "e8e7ee4b8b106d4c5fde9e422a321b2b8a2d5cca546c97adcce927f3e1d36e36"
      }
    }
  ],
  "invocation": {
    "configSource": {
      "entryPoint": "router.Dockerfile"
    },
    "parameters": {
      "frontend": "gateway.v0",
      "args": {
        "cmdline": "docker/dockerfile:1.22",
        "label:org.opencontainers.image.created": "2026-07-21T18:40:29.158Z",
        "label:org.opencontainers.image.description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
        "label:org.opencontainers.image.licenses": "MIT",
        "label:org.opencontainers.image.revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
        "label:org.opencontainers.image.source": "https://github.com/graphql-hive/router",
        "label:org.opencontainers.image.title": "router",
        "label:org.opencontainers.image.url": "https://github.com/graphql-hive/router",
        "label:org.opencontainers.image.vendor": "theguild",
        "label:org.opencontainers.image.version": "pr-1284",
        "source": "docker/dockerfile:1.22"
      },
      "locals": [
        {
          "name": "context"
        },
        {
          "name": "dockerfile"
        }
      ],
      "root": {
        "configSource": {
          "path": "router.Dockerfile"
        },
        "request": {
          "args": {
            "label:org.opencontainers.image.created": "2026-07-21T18:40:29.158Z",
            "label:org.opencontainers.image.description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
            "label:org.opencontainers.image.licenses": "MIT",
            "label:org.opencontainers.image.revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
            "label:org.opencontainers.image.source": "https://github.com/graphql-hive/router",
            "label:org.opencontainers.image.title": "router",
            "label:org.opencontainers.image.url": "https://github.com/graphql-hive/router",
            "label:org.opencontainers.image.vendor": "theguild",
            "label:org.opencontainers.image.version": "pr-1284",
            "vcs:localdir:context": ".",
            "vcs:localdir:dockerfile": "docker",
            "vcs:revision": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
            "vcs:source": "https://github.com/graphql-hive/router"
          }
        }
      },
      "compatibilityVersion": 30
    },
    "environment": {
      "github_actor": "enisdenjo",
      "github_actor_id": "11807600",
      "github_event_name": "pull_request",
      "github_event_payload": {
        "action": "synchronize",
        "after": "5b665bc8a4b61026ffb1bc05c721a516b475f343",
        "before": "d1d3845513e7b07401a41f74d256700c85f5988f",
        "enterprise": {
          "avatar_url": "https://avatars.githubusercontent.com/b/187753?v=4",
          "created_at": "2024-07-02T08:52:28Z",
          "description": "",
          "html_url": "https://github.com/enterprises/the-guild",
          "id": 187753,
          "name": "The Guild",
          "node_id": "E_kgDOAALdaQ",
          "slug": "the-guild",
          "updated_at": "2026-07-11T07:16:45Z",
          "website_url": "https://the-guild.dev/"
        },
        "number": 1284,
        "organization": {
          "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
          "description": "Schema registry, analytics and gateway for GraphQL federation and other GraphQL APIs.",
          "events_url": "https://api.github.com/orgs/graphql-hive/events",
          "hooks_url": "https://api.github.com/orgs/graphql-hive/hooks",
          "id": 182742256,
          "issues_url": "https://api.github.com/orgs/graphql-hive/issues",
          "login": "graphql-hive",
          "members_url": "https://api.github.com/orgs/graphql-hive/members{/member}",
          "node_id": "O_kgDOCuRs8A",
          "public_members_url": "https://api.github.com/orgs/graphql-hive/public_members{/member}",
          "repos_url": "https://api.github.com/orgs/graphql-hive/repos",
          "url": "https://api.github.com/orgs/graphql-hive"
        },
        "pull_request": {
          "_links": {
            "comments": {
              "href": "https://api.github.com/repos/graphql-hive/router/issues/1284/comments"
            },
            "commits": {
              "href": "https://api.github.com/repos/graphql-hive/router/pulls/1284/commits"
            },
            "html": {
              "href": "https://github.com/graphql-hive/router/pull/1284"
            },
            "issue": {
              "href": "https://api.github.com/repos/graphql-hive/router/issues/1284"
            },
            "review_comment": {
              "href": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}"
            },
            "review_comments": {
              "href": "https://api.github.com/repos/graphql-hive/router/pulls/1284/comments"
            },
            "self": {
              "href": "https://api.github.com/repos/graphql-hive/router/pulls/1284"
            },
            "statuses": {
              "href": "https://api.github.com/repos/graphql-hive/router/statuses/5b665bc8a4b61026ffb1bc05c721a516b475f343"
            }
          },
          "active_lock_reason": null,
          "additions": 2435,
          "assignee": null,
          "assignees": [],
          "author_association": "MEMBER",
          "auto_merge": null,
          "base": {
            "label": "graphql-hive:main",
            "ref": "main",
            "repo": {
              "allow_auto_merge": false,
              "allow_forking": true,
              "allow_merge_commit": false,
              "allow_rebase_merge": false,
              "allow_squash_merge": true,
              "allow_update_branch": true,
              "archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
              "archived": false,
              "assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
              "blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
              "branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
              "clone_url": "https://github.com/graphql-hive/router.git",
              "collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
              "comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
              "commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
              "compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
              "contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
              "contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
              "created_at": "2024-11-20T16:16:12Z",
              "default_branch": "main",
              "delete_branch_on_merge": true,
              "deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
              "description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
              "disabled": false,
              "downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
              "events_url": "https://api.github.com/repos/graphql-hive/router/events",
              "fork": false,
              "forks": 16,
              "forks_count": 16,
              "forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
              "full_name": "graphql-hive/router",
              "git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
              "git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
              "git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
              "git_url": "git://github.com/graphql-hive/router.git",
              "has_discussions": false,
              "has_downloads": false,
              "has_issues": true,
              "has_pages": false,
              "has_projects": false,
              "has_pull_requests": true,
              "has_wiki": false,
              "homepage": "https://the-guild.dev/graphql/hive/docs/router",
              "hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
              "html_url": "https://github.com/graphql-hive/router",
              "id": 891604244,
              "is_template": false,
              "issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
              "issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
              "issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
              "keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
              "labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
              "language": "Rust",
              "languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
              "license": {
                "key": "mit",
                "name": "MIT License",
                "node_id": "MDc6TGljZW5zZTEz",
                "spdx_id": "MIT",
                "url": "https://api.github.com/licenses/mit"
              },
              "merge_commit_message": "PR_TITLE",
              "merge_commit_title": "MERGE_MESSAGE",
              "merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
              "milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
              "mirror_url": null,
              "name": "router",
              "node_id": "R_kgDONSTNFA",
              "notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
              "open_issues": 62,
              "open_issues_count": 62,
              "owner": {
                "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
                "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
                "followers_url": "https://api.github.com/users/graphql-hive/followers",
                "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
                "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
                "gravatar_id": "",
                "html_url": "https://github.com/graphql-hive",
                "id": 182742256,
                "login": "graphql-hive",
                "node_id": "O_kgDOCuRs8A",
                "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
                "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
                "repos_url": "https://api.github.com/users/graphql-hive/repos",
                "site_admin": false,
                "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
                "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
                "type": "Organization",
                "url": "https://api.github.com/users/graphql-hive",
                "user_view_type": "public"
              },
              "private": false,
              "pull_request_creation_policy": "all",
              "pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
              "pushed_at": "2026-07-21T18:20:18Z",
              "releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
              "size": 10502,
              "squash_merge_commit_message": "PR_BODY",
              "squash_merge_commit_title": "PR_TITLE",
              "ssh_url": "git@github.com:graphql-hive/router.git",
              "stargazers_count": 94,
              "stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
              "statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
              "subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
              "subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
              "svn_url": "https://github.com/graphql-hive/router",
              "tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
              "teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
              "topics": [
                "apollo-federation",
                "federation",
                "federation-gateway",
                "graphql",
                "graphql-federation",
                "router"
              ],
              "trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
              "updated_at": "2026-07-21T08:44:41Z",
              "url": "https://api.github.com/repos/graphql-hive/router",
              "use_squash_pr_title_as_default": true,
              "visibility": "public",
              "watchers": 94,
              "watchers_count": 94,
              "web_commit_signoff_required": false
            },
            "sha": "a822fe117fb035d300bed69f28b140bf57efb446",
            "user": {
              "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
              "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
              "followers_url": "https://api.github.com/users/graphql-hive/followers",
              "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
              "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
              "gravatar_id": "",
              "html_url": "https://github.com/graphql-hive",
              "id": 182742256,
              "login": "graphql-hive",
              "node_id": "O_kgDOCuRs8A",
              "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
              "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
              "repos_url": "https://api.github.com/users/graphql-hive/repos",
              "site_admin": false,
              "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
              "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
              "type": "Organization",
              "url": "https://api.github.com/users/graphql-hive",
              "user_view_type": "public"
            }
          },
          "body": "Supersedes and therefore closes graphql-hive/router#1269, closes graphql-hive/router#1137, addresses graphql-hive/router#524 through a plugin\n\nAllow a plugin to select a supergraph document during on_http_request and have it hold for each HTTP request or WebSocket upgrade. The selected supergraph is used consistently across validation, introspection, normalization, planning, authorization, demand control, execution, coprocessors, usage reporting, subscriptions, and request deduplication.\n\nThe implementation separates two kinds of state that were previously bundled together:\n\n* `Supergraph` (executor) owns schema-derived data that a plugin can safely construct and retain\n* `RouterSupergraphRuntime` (router) owns router-specific state derived from both that supergraph and the router configuration\n\nConfigured supergraphs still build their runtime eagerly before publication. Plugin-selected supergraphs build their runtime lazily on first use and reuse it through a bounded router-owned cache.\n\nAlso adding `supergraph.source: plugin`, where there is deliberately no configured fallback. A plugin must select a supergraph for each request that needs one.\n\n# Executor (public)\n\n## `Supergraph`\n\n`Supergraph` is the public owner handle. Plugins construct it with either:\n\n```rust\nSupergraph::from_sdl(...)\nSupergraph::from_document(...)\n```\n\nConstruction only accepts query-planner options. It does not accept router configuration, telemetry, callback subscription maps, HTTP clients, or other router-owned state.\n\nThe schema-derived data includes:\n\n* The parsed supergraph document\n* The public consumer schema and its SDL\n* The query planner and planner supergraph state\n* Schema metadata\n* A process-local cache identity\n\nPlugins retain an `Arc<Supergraph>` for as long as that variant should remain selectable. `OnHttpRequestHookPayload::set_supergraph` immediately takes a `SupergraphSnapshot` and stores that snapshot in request extensions. The router does not retain the plugin's `Arc<Supergraph>` owner.\n\nThis distinction is important. A request snapshot keeps the immutable schema data alive, but it does not keep the owner alive. The plugin remains the authority over whether new requests can select a variant!\n\n## Retirement\n\n`Supergraph` owns a `CancellationToken`. `SupergraphSnapshot` receives a clone of that token. When the last `Arc<Supergraph>` is dropped, `Supergraph::drop` cancels the token.\n\nThis gives retirement the required ownership semantics without callbacks from the executor crate into the router:\n\n1. The plugin removes or replaces an `Arc<Supergraph>` from its own state\n2. New requests can no longer obtain that owner\n3. Dropping the final owner publishes retirement\n4. Ordinary requests that already have a snapshot can finish\n5. Active subscriptions observe retirement and close with the schema-reload error\n6. The router's background cleanup task removes the retired runtime from its cache\n\nA snapshot or runtime never delays owner retirement. Conversely, retiring the owner does not invalidate immutable schema data already held by an ordinary in-flight request (they can finish).\n\n# Router (internal)\n\n## `RouterSupergraphRuntime`\n\n`RouterSupergraphRuntime` contains state that cannot be built by a plugin because it depends on router configuration or router-owned infrastructure:\n\n* The subgraph executor map\n* Operation-name forwarding configuration\n* Authorization metadata used by the router pipeline\n* Validation cache\n* Normalization cache\n* Query-plan cache\n* Demand-control runtime and formula cache\n\nRuntime construction receives a `SupergraphSnapshot`, the router configuration, telemetry, and the shared callback subscriptions map. It retains neither the `Supergraph` owner nor the snapshot. Requests and streams carry the snapshot separately.\n\nThis split keeps the public `Supergraph` portable and prevents plugins from having to construct fake `HiveRouterConfig`, `TelemetryContext`, callback maps, or router state.\n\n## Why caches live on the runtime\n\nMy original design shared schema-aware caches keyed by a supergraph cache ID. The final implementation instead places every schema-derived cache directly on its `RouterSupergraphRuntime`.\n\nThis is simpler and gives the same isolation guarantee structurally:\n\n* Two distinct `Supergraph` instances cannot share validation, normalization, plan, or demand-control formula entries because they have different runtimes\n* Cache keys dont need a supergraph ID because the cache itself belongs to exactly one runtime\n* Retiring or evicting a runtime naturally retires its cache entries with it\n* Configured schema rotation does not globally invalidate unrelated plugin caches\n* Old configured caches remain available only to requests or streams still holding the old runtime\n\nNote that the parse cache remains shared because parsing a GraphQL operation is schema-independent!\n\nThe process-local supergraph ID is still required for two purposes:\n\n* Looking up a plugin-selected runtime in the runtime cache\n* Partitioning in-flight request deduplication so identical operations against distinct supergraph instances never join the same execution\n\nIt's important to know that the ID is instance identity, not content identity! Two separately constructed supergraphs receive different IDs even if their SDL or consumer schemas are identical.\n\n# Configured and plugin-selected runtimes\n\n## Configured supergraph\n\n`SchemaState` has one atomic configured slot containing:\n\n* The configured `Arc<Supergraph>` owner\n* Its `SupergraphSnapshot`\n* Its eagerly built `Arc<RouterSupergraphRuntime>`\n\nThese values are published together with `ArcSwap`. A request can therefore never observe a schema from one generation and a runtime from another.\n\nThe configured runtime does not participate in the bounded plugin runtime cache. The current configured value is pinned by the configured slot and is never evicted. It follows the same ownership and retirement rules as a plugin variant, but its owner is retained by `SchemaState` instead of a plugin.\n\nConfigured loading and rotation follow this order:\n\n1. Load and parse the new supergraph SDL\n2. Run the existing supergraph load hooks\n3. Construct the schema-only `Supergraph`\n4. Run end hooks that may replace it\n5. Build `RouterSupergraphRuntime`\n6. Atomically publish owner, snapshot, and runtime\n7. Drop the previous configured owner after the swap\n\nIf schema or runtime construction fails, the current configured value remains untouched. A successful swap retires only the previous configured generation.\n\nSwapping no longer clears the complete shared schema caches or globally closes all subscriptions because dropping the old supergraph wil retire it and that will clean up its own cache and related subscriptions.\n\n## Plugin-selected supergraph\n\nPlugin runtimes are built lazily because the router cannot know which variants a plugin will select. `SchemaState` uses a `Mutex<VecDeque<...>>` as a strict FIFO runtime cache:\n\n* Maximum size is 10\n* The same supergraph ID reuses the same runtime\n* Cache hits do not change insertion order\n* Runtime construction is serialized under the mutex, so concurrent first requests build once\n* Failed construction is not inserted\n* Selecting a broken plugin variant fails closed and never falls back to the configured supergraph\n* Inserting an eleventh live variant evicts the oldest cached runtime\n\nFIFO eviction only removes the router's cached `Arc<RouterSupergraphRuntime>`. It does not drop the plugin's `Arc<Supergraph>`, publish retirement, or close subscriptions. If the plugin selects that still-live supergraph again, the router will rebuild its runtime.\n\n# Background runtime cleanup\n\nA router-managed `RuntimeCacheCleanupTask` receives registrations when plugin runtimes enter the cache. Each registration includes the supergraph ID and a clone of its retirement token. The task waits for retirement and removes the matching runtime entry.\n\nFIFO eviction sends a corresponding eviction message so the task can cancel and discard a waiter that no longer has a cached runtime to clean up. Registration is deduplicated by supergraph ID, including the case where a still-live supergraph is evicted and later inserted again.\n\nThis task has deliberately narrow authority:\n\n* Owner retirement can remove a runtime cache entry\n* FIFO eviction can remove a runtime cache entry\n* Runtime removal cannot retire a `Supergraph`\n* Runtime removal cannot stop an ordinary request or stream that already holds its own runtime `Arc`\n\nThe cache is still bounded if the cleanup task is unavailable. The task exists to release retired executors and schema caches earlier, not to provide the memory bound.\n\nConfigured runtimes do not need cleanup-task registration. Replacing the atomic configured slot immediately releases the slot's old runtime reference therefore dropping it. Requests and streams keep it alive only for as long as they still use it.\n\n## Eviction != retirement\n\nRuntime eviction and supergraph retirement are deliberately separate lifecycle events.\n\nThe plugin runtime cache stores only:\n\n```\nsupergraph ID -> Arc<RouterSupergraphRuntime>\n```\n\nIt does not store the owning `Arc<Supergraph>` or a complete `SelectedSupergraph`.\n\nEviction removes only the cache's `Arc<RouterSupergraphRuntime>`. It does not drop the plugin's `Arc<Supergraph>`, cancel the supergraph's retirement token, make the supergraph unselectable, or close its subscriptions.\n\nA running request or subscription holds its own SelectedSupergraph, which contains:\n\n* The `SupergraphSnapshot`\n* An `Arc<RouterSupergraphRuntime>`\n\nTherefore, evicting the cached runtime does not invalidate work already using it. The subscription's runtime remains alive through its own Arc, and the subscription continues normally.\n\nIf the plugin still owns the Supergraph and selects it again after its runtime was evicted, the router builds and caches a new runtime. Both runtimes are derived from the same immutable supergraph and router configuration, so this temporary overlap is safe.\n\nSubscriptions close with `SUBSCRIPTION_SCHEMA_RELOAD` **only** when the selected supergraph retires. Retirement occurs when the final owning Arc is dropped. `Supergraph::drop` cancels its retirement token, and subscription producers observing that token broadcast the reload error and stop. Existing subscriptions may continue using the previous runtime while new requests use the rebuilt runtime.\n\nThe background cleanup task may remove a cached runtime after its owner retires, but that removal is a consequence of retirement, not its cause. The retirement token closes subscriptions; cache cleanup only releases the router's cached runtime reference.\n\n# Request selection and consistency\n\n`SchemaState::select_supergraph` applies one selection rule:\n\n1. Reuse a `SelectedSupergraph` already pinned to the request (selecting multiple times throught)\n2. Otherwise prefer a plugin-provided `SupergraphSnapshot` from request extensions (plugin set)\n3. Otherwise use the configured owner, snapshot, and runtime (configuration set)\n4. If neither exists, return no selection\n5. If a plugin runtime cannot be built, return the runtime error without falling back\n   * Deliberate decision, if a plugin provided a supergraph but cant be built - it would be a security issue to fall back to the configured one...\n\n`SelectedSupergraph` contains the exact snapshot and runtime pair. It is stored back into request extensions for both plugin and configured selections. This makes later users consume the same generation rather than reading the global configured slot again.\n\nThe selected pair is used by:\n\n* Validation and validation plugins\n* Progressive override state\n* GraphQL request and analysis coprocessors\n* Normalization\n* Variable coercion\n* Authorization\n* Query planning and query-plan plugins\n* Demand control\n* Introspection\n* Subgraph execution\n* Operation-name forwarding\n* Usage reporting\n* In-flight request deduplication\n* The GraphQL response coprocessor\n\nThe response coprocessor reads the pinned request selection. This matters during configured rotation because re-reading the current configured slot after execution could expose SDL from a newer generation than the one that produced the response.\n\n# Ordinary requests, deduplication, and subscriptions\n\n## Ordinary requests\n\nAn ordinary request keeps `SelectedSupergraph` alive through execution and response processing. If the owner retires during the request, the snapshot and runtime remain valid and the request completes normally.\n\nThis avoids aborting safe immutable work while still preventing future requests from selecting a removed owner.\n\n## In-flight request deduplication\n\nThe request fingerprint now includes the selected supergraph's instance ID instead of the consumer-schema checksum. Consumer schemas can be identical while routing supergraphs, subgraph endpoints, planner state, or ownership lifetimes differ.\n\nThis prevents requests for different supergraph instances from sharing an in-flight execution. Deduplicated subscription consumers still share one producer only when they selected the same supergraph instance and have the same remaining fingerprint inputs.\n\n## Subscription retirement\n\nThe subscription producer pump retains the complete `SelectedSupergraph`, not only its token. This keeps the selected snapshot and router runtime alive for the full stream lifetime, including after the HTTP handler has returned streaming headers.\n\nThe pump selects between the next upstream item and `SupergraphSnapshot::retired()`. Retirement broadcasts the existing error and stops the producer like before during schema reload.\n\nEach closes only subscriptions selected from the retired owner. The previous global `close_all_with_error` call on configured reload is removed.\n\n## WebSockets\n\nA valid WebSocket upgrade resolves and pins its `SelectedSupergraph` during the first message (not during the HTTP upgrade).\n\nAn upgrade without any selected or configured supergraph rejecting with HTTP 503 would be invisible to browsers because browsers dont provide insights to the WebSocket client about why the HTTP Upgrade failed.\n\nWe therefore instead accept the WebSocket connection and close it with a specific close code and message surfacing the error to the clients and giving insight into what happened.\n\nIf that supergraph later retires:\n\n* Existing subscription producers terminate through the retirement token\n* New operations on the connection are rejected as unavailable (explained above)\n* The connection cannot silently switch to a newly configured generation\n\nThis is the same lifetime rule as an ordinary request, extended to the WebSocket connection instead of one operation.\n\n# `supergraph.source: plugin`\n\nThe new configuration is:\n\n```yaml\nsupergraph:\n  source: plugin\n```\n\nThis mode means there is no configured supergraph:\n\n* No file, Hive, or storage loader is created\n* No supergraph polling task is registered\n* The configured slot remains empty\n* Plugins, telemetry, caches, callback handling, and the HTTP server still initialize normally\n* A GraphQL request without a plugin-selected supergraph returns `NO_SUPERGRAPH_AVAILABLE` with HTTP 503\n* A WebSocket connection without a plugin-selected supergraph is closed with `No supergraph available yet`\n* A selected supergraph whose runtime cannot be built returns an internal runtime error\n\nExisting environment overrides for file and Hive sources continue to use the existing configuration override behavior. No plugin-source environment variable is added.\n\n# Health, readiness, and Prometheus\n\nHealth and readiness now pass through the plugin `on_http_request` chain and its `on_end` callbacks. This is required because readiness in plugin-only mode must allow the plugin to select a supergraph for that specific readiness request.\n\nProbe behavior is:\n\n* Health reports process liveness and remains 200 unless a plugin explicitly changes or ends the request\n* Readiness resolves the request's plugin-selected supergraph first and the configured fallback second\n* Readiness is 200 only when the selected supergraph is not retired and has a usable runtime\n* Missing selection or runtime construction failure produces 503\n* Coprocessors do not run for health or readiness\n* Prometheus keeps bypassing both plugins and coprocessors\n\nReadiness selection is request-local. A plugin selecting a supergraph for one readiness request does not publish it as a configured default.\n\n# Callback subscriptions\n\nAll configured and plugin-selected runtimes receive the same router-owned callback subscriptions map. The router also runs one heartbeat enforcer over that map.\n\nPlugins never construct or own this infrastructure. Sharing the map preserves callback routing and heartbeat enforcement regardless of which supergraph a request selected.\n\n# Hook and error ownership\n\nSupergraph load hooks continue to apply only to configured-source loading. They receive schema state without router-only executor state, and a plugin-selected `Supergraph` does not trigger configured reload hooks.\n\nError ownership follows the schema/runtime split:\n\n* SDL parsing and planner construction errors come from `Supergraph` construction\n* Subgraph executor and authorization runtime construction errors come from `RouterSupergraphRuntime` construction\n* Missing selection uses `PipelineError::NoSupergraphAvailable`\n* A plugin runtime build failure uses `PipelineError::RouterSupergraphRuntimeError` and is not cached\n\n`OnGraphQLValidationStartHookPayload::with_schema` is removed because it cannot provide pipeline-wide consistency. Plugins should migrate to `set_supergraph` in `on_http_request`.\n\n# Examples\n\n## `plugin_examples/replace_schema`\n\nThis example has a normal configured file supergraph. The plugin builds and retains one additional `Arc<Supergraph>` during initialization.\n\nRequests without the `x-schema-variant: basic` header use the configured default. Requests with that header select the plugin's stripped `basic` supergraph. The example demonstrates request-local override with a configured fallback, including validation and introspection behavior.\n\nIt also covers the fail-closed rule: selecting a variant whose router runtime cannot be constructed returns a runtime error instead of falling back to the configured schema.\n\n## `plugin_examples/feature_flags`\n\nThis example uses:\n\n```yaml\nsupergraph:\n  source: plugin\n```\n\nThere is no configured default. The plugin owns the base document and a map of `Arc<Supergraph>` variants. It normalizes the feature-flag header, constructs each variant once, retains it in the map, and selects it with `set_supergraph`.\n\nEvery GraphQL request and readiness request must receive a plugin selection. If the plugin intentionally skips selection, the router returns `NO_SUPERGRAPH_AVAILABLE`. The example demonstrates full plugin ownership and switching among multiple schemas without any configured fallback.\n\nTogether the examples cover both supported models: override a configured default, or make the plugin the only supergraph source.\n\n# Intentional tradeoffs\n\n* Runtime construction is serialized on cache misses. Misses should be rare, and this guarantees one construction without another single-flight abstraction\n* The plugin runtime cache uses strict FIFO with a fixed capacity of 10. Hits do not refresh order, keeping behavior deterministic\n* Failed runtime construction is retried on a later request. Negative caching is deferred until repeated deterministic failures are shown to be a real problem\n* Retirement is based on owner lifetime, not SDL equality. Rebuilding identical SDL creates a new lifecycle and a new deduplication identity\n* Ordinary in-flight requests are allowed to finish after retirement. Only long-lived subscription work is actively terminated\n* The configured runtime is not placed in the plugin FIFO cache. Its atomic slot already provides the required permanent pin and rotation boundary\n\n# TODOs\n\n- [ ] docs\n- [ ] ~~rename ~~`~~SchemaState~~`~~ because its confusing, or merge it with ~~`~~RouterSharedState~~` for later times\n- [X] rename `new_supergraph_data` in `OnSupergraphLoadEndHookPayload` to just `new_supergraph` because it points to `Supergraph`. or should it point to `SupergraphData` still?\n- [X] (we shouldnt) should we expose the actual supergraph runtime error? I think it should be exposed unless masking is disabled (future)\n- [ ] ~~subgraph url overrides should have sort of a label that will allow users to override with dynamic supergraphs~~ think and decide about labeling in the future. for now, when the plugins are the only one that can dhynamically switch the supergraph - they have full control by managing their own map of the supergraphs, i.e. subgraph url overrides can happen directly in the plugin",
          "changed_files": 43,
          "closed_at": null,
          "comments": 3,
          "comments_url": "https://api.github.com/repos/graphql-hive/router/issues/1284/comments",
          "commits": 42,
          "commits_url": "https://api.github.com/repos/graphql-hive/router/pulls/1284/commits",
          "created_at": "2026-07-15T22:43:28Z",
          "deletions": 623,
          "diff_url": "https://github.com/graphql-hive/router/pull/1284.diff",
          "draft": false,
          "head": {
            "label": "graphql-hive:super-replace-schemastate",
            "ref": "super-replace-schemastate",
            "repo": {
              "allow_auto_merge": false,
              "allow_forking": true,
              "allow_merge_commit": false,
              "allow_rebase_merge": false,
              "allow_squash_merge": true,
              "allow_update_branch": true,
              "archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
              "archived": false,
              "assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
              "blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
              "branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
              "clone_url": "https://github.com/graphql-hive/router.git",
              "collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
              "comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
              "commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
              "compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
              "contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
              "contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
              "created_at": "2024-11-20T16:16:12Z",
              "default_branch": "main",
              "delete_branch_on_merge": true,
              "deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
              "description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
              "disabled": false,
              "downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
              "events_url": "https://api.github.com/repos/graphql-hive/router/events",
              "fork": false,
              "forks": 16,
              "forks_count": 16,
              "forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
              "full_name": "graphql-hive/router",
              "git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
              "git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
              "git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
              "git_url": "git://github.com/graphql-hive/router.git",
              "has_discussions": false,
              "has_downloads": false,
              "has_issues": true,
              "has_pages": false,
              "has_projects": false,
              "has_pull_requests": true,
              "has_wiki": false,
              "homepage": "https://the-guild.dev/graphql/hive/docs/router",
              "hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
              "html_url": "https://github.com/graphql-hive/router",
              "id": 891604244,
              "is_template": false,
              "issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
              "issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
              "issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
              "keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
              "labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
              "language": "Rust",
              "languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
              "license": {
                "key": "mit",
                "name": "MIT License",
                "node_id": "MDc6TGljZW5zZTEz",
                "spdx_id": "MIT",
                "url": "https://api.github.com/licenses/mit"
              },
              "merge_commit_message": "PR_TITLE",
              "merge_commit_title": "MERGE_MESSAGE",
              "merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
              "milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
              "mirror_url": null,
              "name": "router",
              "node_id": "R_kgDONSTNFA",
              "notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
              "open_issues": 62,
              "open_issues_count": 62,
              "owner": {
                "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
                "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
                "followers_url": "https://api.github.com/users/graphql-hive/followers",
                "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
                "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
                "gravatar_id": "",
                "html_url": "https://github.com/graphql-hive",
                "id": 182742256,
                "login": "graphql-hive",
                "node_id": "O_kgDOCuRs8A",
                "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
                "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
                "repos_url": "https://api.github.com/users/graphql-hive/repos",
                "site_admin": false,
                "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
                "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
                "type": "Organization",
                "url": "https://api.github.com/users/graphql-hive",
                "user_view_type": "public"
              },
              "private": false,
              "pull_request_creation_policy": "all",
              "pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
              "pushed_at": "2026-07-21T18:20:18Z",
              "releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
              "size": 10502,
              "squash_merge_commit_message": "PR_BODY",
              "squash_merge_commit_title": "PR_TITLE",
              "ssh_url": "git@github.com:graphql-hive/router.git",
              "stargazers_count": 94,
              "stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
              "statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
              "subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
              "subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
              "svn_url": "https://github.com/graphql-hive/router",
              "tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
              "teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
              "topics": [
                "apollo-federation",
                "federation",
                "federation-gateway",
                "graphql",
                "graphql-federation",
                "router"
              ],
              "trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
              "updated_at": "2026-07-21T08:44:41Z",
              "url": "https://api.github.com/repos/graphql-hive/router",
              "use_squash_pr_title_as_default": true,
              "visibility": "public",
              "watchers": 94,
              "watchers_count": 94,
              "web_commit_signoff_required": false
            },
            "sha": "5b665bc8a4b61026ffb1bc05c721a516b475f343",
            "user": {
              "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
              "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
              "followers_url": "https://api.github.com/users/graphql-hive/followers",
              "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
              "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
              "gravatar_id": "",
              "html_url": "https://github.com/graphql-hive",
              "id": 182742256,
              "login": "graphql-hive",
              "node_id": "O_kgDOCuRs8A",
              "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
              "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
              "repos_url": "https://api.github.com/users/graphql-hive/repos",
              "site_admin": false,
              "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
              "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
              "type": "Organization",
              "url": "https://api.github.com/users/graphql-hive",
              "user_view_type": "public"
            }
          },
          "html_url": "https://github.com/graphql-hive/router/pull/1284",
          "id": 4064208307,
          "issue_url": "https://api.github.com/repos/graphql-hive/router/issues/1284",
          "labels": [],
          "locked": false,
          "maintainer_can_modify": false,
          "merge_commit_sha": "8670aa63c5afc721ef7464959d76115224dedb03",
          "mergeable": null,
          "mergeable_state": "unknown",
          "merged": false,
          "merged_at": null,
          "merged_by": null,
          "milestone": null,
          "node_id": "PR_kwDONSTNFM7yPuWz",
          "number": 1284,
          "patch_url": "https://github.com/graphql-hive/router/pull/1284.patch",
          "rebaseable": null,
          "requested_reviewers": [
            {
              "avatar_url": "https://avatars.githubusercontent.com/u/8167190?v=4",
              "events_url": "https://api.github.com/users/kamilkisiela/events{/privacy}",
              "followers_url": "https://api.github.com/users/kamilkisiela/followers",
              "following_url": "https://api.github.com/users/kamilkisiela/following{/other_user}",
              "gists_url": "https://api.github.com/users/kamilkisiela/gists{/gist_id}",
              "gravatar_id": "",
              "html_url": "https://github.com/kamilkisiela",
              "id": 8167190,
              "login": "kamilkisiela",
              "node_id": "MDQ6VXNlcjgxNjcxOTA=",
              "organizations_url": "https://api.github.com/users/kamilkisiela/orgs",
              "received_events_url": "https://api.github.com/users/kamilkisiela/received_events",
              "repos_url": "https://api.github.com/users/kamilkisiela/repos",
              "site_admin": false,
              "starred_url": "https://api.github.com/users/kamilkisiela/starred{/owner}{/repo}",
              "subscriptions_url": "https://api.github.com/users/kamilkisiela/subscriptions",
              "type": "User",
              "url": "https://api.github.com/users/kamilkisiela",
              "user_view_type": "public"
            }
          ],
          "requested_teams": [],
          "review_comment_url": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}",
          "review_comments": 21,
          "review_comments_url": "https://api.github.com/repos/graphql-hive/router/pulls/1284/comments",
          "state": "open",
          "statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/5b665bc8a4b61026ffb1bc05c721a516b475f343",
          "title": "feat(router): request selected supergraphs",
          "updated_at": "2026-07-21T18:20:20Z",
          "url": "https://api.github.com/repos/graphql-hive/router/pulls/1284",
          "user": {
            "avatar_url": "https://avatars.githubusercontent.com/u/11807600?v=4",
            "events_url": "https://api.github.com/users/enisdenjo/events{/privacy}",
            "followers_url": "https://api.github.com/users/enisdenjo/followers",
            "following_url": "https://api.github.com/users/enisdenjo/following{/other_user}",
            "gists_url": "https://api.github.com/users/enisdenjo/gists{/gist_id}",
            "gravatar_id": "",
            "html_url": "https://github.com/enisdenjo",
            "id": 11807600,
            "login": "enisdenjo",
            "node_id": "MDQ6VXNlcjExODA3NjAw",
            "organizations_url": "https://api.github.com/users/enisdenjo/orgs",
            "received_events_url": "https://api.github.com/users/enisdenjo/received_events",
            "repos_url": "https://api.github.com/users/enisdenjo/repos",
            "site_admin": false,
            "starred_url": "https://api.github.com/users/enisdenjo/starred{/owner}{/repo}",
            "subscriptions_url": "https://api.github.com/users/enisdenjo/subscriptions",
            "type": "User",
            "url": "https://api.github.com/users/enisdenjo",
            "user_view_type": "public"
          }
        },
        "repository": {
          "allow_forking": true,
          "archive_url": "https://api.github.com/repos/graphql-hive/router/{archive_format}{/ref}",
          "archived": false,
          "assignees_url": "https://api.github.com/repos/graphql-hive/router/assignees{/user}",
          "blobs_url": "https://api.github.com/repos/graphql-hive/router/git/blobs{/sha}",
          "branches_url": "https://api.github.com/repos/graphql-hive/router/branches{/branch}",
          "clone_url": "https://github.com/graphql-hive/router.git",
          "collaborators_url": "https://api.github.com/repos/graphql-hive/router/collaborators{/collaborator}",
          "comments_url": "https://api.github.com/repos/graphql-hive/router/comments{/number}",
          "commits_url": "https://api.github.com/repos/graphql-hive/router/commits{/sha}",
          "compare_url": "https://api.github.com/repos/graphql-hive/router/compare/{base}...{head}",
          "contents_url": "https://api.github.com/repos/graphql-hive/router/contents/{+path}",
          "contributors_url": "https://api.github.com/repos/graphql-hive/router/contributors",
          "created_at": "2024-11-20T16:16:12Z",
          "custom_properties": {
            "vanta_production_branch_name": "main"
          },
          "default_branch": "main",
          "deployments_url": "https://api.github.com/repos/graphql-hive/router/deployments",
          "description": "Open-source (MIT) GraphQL Federation Router. Built with Rust for maximum performance and robustness.",
          "disabled": false,
          "downloads_url": "https://api.github.com/repos/graphql-hive/router/downloads",
          "events_url": "https://api.github.com/repos/graphql-hive/router/events",
          "fork": false,
          "forks": 16,
          "forks_count": 16,
          "forks_url": "https://api.github.com/repos/graphql-hive/router/forks",
          "full_name": "graphql-hive/router",
          "git_commits_url": "https://api.github.com/repos/graphql-hive/router/git/commits{/sha}",
          "git_refs_url": "https://api.github.com/repos/graphql-hive/router/git/refs{/sha}",
          "git_tags_url": "https://api.github.com/repos/graphql-hive/router/git/tags{/sha}",
          "git_url": "git://github.com/graphql-hive/router.git",
          "has_discussions": false,
          "has_downloads": false,
          "has_issues": true,
          "has_pages": false,
          "has_projects": false,
          "has_pull_requests": true,
          "has_wiki": false,
          "homepage": "https://the-guild.dev/graphql/hive/docs/router",
          "hooks_url": "https://api.github.com/repos/graphql-hive/router/hooks",
          "html_url": "https://github.com/graphql-hive/router",
          "id": 891604244,
          "is_template": false,
          "issue_comment_url": "https://api.github.com/repos/graphql-hive/router/issues/comments{/number}",
          "issue_events_url": "https://api.github.com/repos/graphql-hive/router/issues/events{/number}",
          "issues_url": "https://api.github.com/repos/graphql-hive/router/issues{/number}",
          "keys_url": "https://api.github.com/repos/graphql-hive/router/keys{/key_id}",
          "labels_url": "https://api.github.com/repos/graphql-hive/router/labels{/name}",
          "language": "Rust",
          "languages_url": "https://api.github.com/repos/graphql-hive/router/languages",
          "license": {
            "key": "mit",
            "name": "MIT License",
            "node_id": "MDc6TGljZW5zZTEz",
            "spdx_id": "MIT",
            "url": "https://api.github.com/licenses/mit"
          },
          "merges_url": "https://api.github.com/repos/graphql-hive/router/merges",
          "milestones_url": "https://api.github.com/repos/graphql-hive/router/milestones{/number}",
          "mirror_url": null,
          "name": "router",
          "node_id": "R_kgDONSTNFA",
          "notifications_url": "https://api.github.com/repos/graphql-hive/router/notifications{?since,all,participating}",
          "open_issues": 62,
          "open_issues_count": 62,
          "owner": {
            "avatar_url": "https://avatars.githubusercontent.com/u/182742256?v=4",
            "events_url": "https://api.github.com/users/graphql-hive/events{/privacy}",
            "followers_url": "https://api.github.com/users/graphql-hive/followers",
            "following_url": "https://api.github.com/users/graphql-hive/following{/other_user}",
            "gists_url": "https://api.github.com/users/graphql-hive/gists{/gist_id}",
            "gravatar_id": "",
            "html_url": "https://github.com/graphql-hive",
            "id": 182742256,
            "login": "graphql-hive",
            "node_id": "O_kgDOCuRs8A",
            "organizations_url": "https://api.github.com/users/graphql-hive/orgs",
            "received_events_url": "https://api.github.com/users/graphql-hive/received_events",
            "repos_url": "https://api.github.com/users/graphql-hive/repos",
            "site_admin": false,
            "starred_url": "https://api.github.com/users/graphql-hive/starred{/owner}{/repo}",
            "subscriptions_url": "https://api.github.com/users/graphql-hive/subscriptions",
            "type": "Organization",
            "url": "https://api.github.com/users/graphql-hive",
            "user_view_type": "public"
          },
          "private": false,
          "pull_request_creation_policy": "all",
          "pulls_url": "https://api.github.com/repos/graphql-hive/router/pulls{/number}",
          "pushed_at": "2026-07-21T18:20:18Z",
          "releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
          "size": 10502,
          "ssh_url": "git@github.com:graphql-hive/router.git",
          "stargazers_count": 94,
          "stargazers_url": "https://api.github.com/repos/graphql-hive/router/stargazers",
          "statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/{sha}",
          "subscribers_url": "https://api.github.com/repos/graphql-hive/router/subscribers",
          "subscription_url": "https://api.github.com/repos/graphql-hive/router/subscription",
          "svn_url": "https://github.com/graphql-hive/router",
          "tags_url": "https://api.github.com/repos/graphql-hive/router/tags",
          "teams_url": "https://api.github.com/repos/graphql-hive/router/teams",
          "topics": [
            "apollo-federation",
            "federation",
            "federation-gateway",
            "graphql",
            "graphql-federation",
            "router"
          ],
          "trees_url": "https://api.github.com/repos/graphql-hive/router/git/trees{/sha}",
          "updated_at": "2026-07-21T08:44:41Z",
          "url": "https://api.github.com/repos/graphql-hive/router",
          "visibility": "public",
          "watchers": 94,
          "watchers_count": 94,
          "web_commit_signoff_required": false
        },
        "sender": {
          "avatar_url": "https://avatars.githubusercontent.com/u/11807600?v=4",
          "events_url": "https://api.github.com/users/enisdenjo/events{/privacy}",
          "followers_url": "https://api.github.com/users/enisdenjo/followers",
          "following_url": "https://api.github.com/users/enisdenjo/following{/other_user}",
          "gists_url": "https://api.github.com/users/enisdenjo/gists{/gist_id}",
          "gravatar_id": "",
          "html_url": "https://github.com/enisdenjo",
          "id": 11807600,
          "login": "enisdenjo",
          "node_id": "MDQ6VXNlcjExODA3NjAw",
          "organizations_url": "https://api.github.com/users/enisdenjo/orgs",
          "received_events_url": "https://api.github.com/users/enisdenjo/received_events",
          "repos_url": "https://api.github.com/users/enisdenjo/repos",
          "site_admin": false,
          "starred_url": "https://api.github.com/users/enisdenjo/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/enisdenjo/subscriptions",
          "type": "User",
          "url": "https://api.github.com/users/enisdenjo",
          "user_view_type": "public"
        }
      },
      "github_job": "docker",
      "github_ref": "refs/pull/1284/merge",
      "github_ref_name": "1284/merge",
      "github_ref_protected": "false",
      "github_ref_type": "branch",
      "github_repository": "graphql-hive/router",
      "github_repository_id": "891604244",
      "github_repository_owner": "graphql-hive",
      "github_repository_owner_id": "182742256",
      "github_run_attempt": "1",
      "github_run_id": "29856788774",
      "github_run_number": "4514",
      "github_runner_arch": "X64",
      "github_runner_environment": "github-hosted",
      "github_runner_image_os": "ubuntu24",
      "github_runner_image_version": "20260714.240.1",
      "github_runner_name": "GitHub Actions 1000922431",
      "github_runner_os": "Linux",
      "github_runner_tracking_id": "github_73f16072-67a6-4c6d-91cb-88ad758944e9",
      "github_server_url": "https://github.com",
      "github_triggering_actor": "enisdenjo",
      "github_workflow": "build-router",
      "github_workflow_ref": "graphql-hive/router/.github/workflows/build-router.yaml@refs/pull/1284/merge",
      "github_workflow_sha": "2a2f734dd0a0e6187d72b2da7d4aa1b5437aec2a",
      "platform": "linux/amd64"
    }
  }
},
"buildx.build.ref": "builder-d3113114-df27-4710-98f0-e55b25592baa/builder-d3113114-df27-4710-98f0-e55b25592baa0/bo129p1the0tk4wmb8kx9r3ts",
"containerimage.descriptor": {
  "mediaType": "application/vnd.oci.image.index.v1+json",
  "digest": "sha256:4c16a50efc81a3a1c1f0cb2436b9b1a43a4a60bf61777ef9b30f37965b4b4e53",
  "size": 1609
},
"containerimage.digest": "sha256:4c16a50efc81a3a1c1f0cb2436b9b1a43a4a60bf61777ef9b30f37965b4b4e53",
"image.name": "ghcr.io/graphql-hive/router:pr-1284,ghcr.io/graphql-hive/router:sha-2a2f734"
}

@enisdenjo
enisdenjo marked this pull request as ready for review July 16, 2026 10:27
# Conflicts:
#	bin/router/src/pipeline/authorization/mod.rs
#	bin/router/src/pipeline/error.rs
#	bin/router/src/pipeline/execution.rs
#	bin/router/src/pipeline/mod.rs
#	bin/router/src/pipeline/normalize.rs
#	plugin_examples/Cargo.lock
#	plugin_examples/Cargo.toml
Comment thread bin/router/src/schema_state.rs
@praguevara

Copy link
Copy Markdown
Contributor

Would it be possible to make on_http_request async? A plugin that picks a supergraph with set_supergraph might need to do some I/O, and it'd be awkward to do if the hook doesn't support awaiting. We can't block on it since ntex's executor isn't a shared thread pool, so it'd stall the whole worker.

@enisdenjo

enisdenjo commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

making on_http_request async make sense for the set_supergraph usecase, but it is not a straightforward signature change... on_http_request's payload contains ntex's raw WebRequest, which is backed by Rc and is not Send - making it not async compatible.

the idea/recommended approach at the moment is to move I/O out of the request hook. load the possible supergraphs during on_plugin_init, or refresh them in a background task and store them in shared plugin state. then on_http_request can synchronously select and clone the appropriate cached supergraph (like the existing replace_schema and feature_flags examples do).

mind you, the I/O use case is very much valid, especially since blocking would stall an ntex worker. we hope to address async support in a follow-up PR where we can make the necessary API and executor decisions deliberately and thoughtfully

@enisdenjo
enisdenjo merged commit 8509a55 into main Jul 21, 2026
31 checks passed
@enisdenjo
enisdenjo deleted the super-replace-schemastate branch July 21, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin system: allow to override schema for the entire execution

5 participants