Conversation
cf0bb48 to
9d9e2df
Compare
Summary of ChangesHello @enisdenjo, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces comprehensive support for GraphQL subscriptions within the router. It fundamentally changes how the router handles long-lived connections and streaming data, moving from a single-response model to one that can manage continuous data flows. This enables real-time data updates for clients and subgraphs, complete with dynamic protocol negotiation and on-the-fly entity resolution for each event in a subscription stream. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces significant new functionality for GraphQL subscriptions, including support for SSE and multipart protocols, entity resolution for subscription events, and updates to the query planner. The overall architecture is well-thought-out, especially the separation of concerns with QueryPlanExecutionResult and the use of an owned context for long-lived subscriptions.
My review has identified a critical safety issue regarding an unsafe block that could lead to a use-after-free bug. I've also found several areas where manual JSON construction is used, which is brittle and should be replaced with safer serialization. Additionally, there are opportunities to improve performance in the stream handling logic by reducing string allocations, and a potential panic in the benchmark code. Please see the detailed comments for suggestions on how to address these points.
✅
|
|
🐋 This PR was built and pushed to the following Docker images: Image Names: Platforms: Image Tags: Docker metadata{
"buildx.build.provenance/linux/amd64": {
"builder": {
"id": "https://github.com/graphql-hive/router/actions/runs/23447240895/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": "329e54034ce498f9c6b345044e8f530c6691f99e94a92446f68c0adf9baa8464"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "router.Dockerfile"
},
"parameters": {
"frontend": "gateway.v0",
"args": {
"cmdline": "docker/dockerfile:1.22",
"label:org.opencontainers.image.created": "2026-03-23T16:19:58.482Z",
"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": "5b300c5ef067668ed53d0d7d90058088f3ba5976",
"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-620",
"source": "docker/dockerfile:1.22"
},
"locals": [
{
"name": "context"
},
{
"name": "dockerfile"
}
]
},
"environment": {
"github_actor": "enisdenjo",
"github_actor_id": "11807600",
"github_event_name": "pull_request",
"github_event_payload": {
"action": "synchronize",
"after": "6cb2e62d38d3570fce6efdee36f86d745ca8d31d",
"before": "f1e9e1e3386eeddc6f86966ec2ed2e3de189145d",
"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-03-11T16:47:15Z",
"website_url": "https://the-guild.dev/"
},
"number": 620,
"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/620/comments"
},
"commits": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/620/commits"
},
"html": {
"href": "https://github.com/graphql-hive/router/pull/620"
},
"issue": {
"href": "https://api.github.com/repos/graphql-hive/router/issues/620"
},
"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/620/comments"
},
"self": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/620"
},
"statuses": {
"href": "https://api.github.com/repos/graphql-hive/router/statuses/6cb2e62d38d3570fce6efdee36f86d745ca8d31d"
}
},
"active_lock_reason": null,
"additions": 8456,
"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": 8,
"forks_count": 8,
"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": true,
"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": 58,
"open_issues_count": 58,
"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-03-23T16:07:10Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 5895,
"squash_merge_commit_message": "PR_BODY",
"squash_merge_commit_title": "PR_TITLE",
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 78,
"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-03-21T17:31:04Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"use_squash_pr_title_as_default": true,
"visibility": "public",
"watchers": 78,
"watchers_count": 78,
"web_commit_signoff_required": false
},
"sha": "72cf0ec5dc15f3540814a753fb4871ea2107054c",
"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": "Closes #313 \r\nRef router-165\r\n\r\nImplement [SSE](https://github.com/enisdenjo/graphql-sse/blob/918b9d414a4f938c715a8f8bc13184c0849eac50/PROTOCOL.md#distinct-connections-mode), [Incremental Delivery over HTTP](https://github.com/graphql/graphql-over-http/blob/d312e43384006fa323b918d49cfd9fbd76ac1257/rfcs/IncrementalDelivery.md), [Apollo's Multipart HTTP](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol), [WebSocket](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) and [HTTP Callback](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/callback-protocol) specs for subscriptions when communicating with subgraphs or clients with entity resolution capabilities.\r\n\r\n\r\n# Streaming execution result\r\n\r\n```\r\nlib/executor/src/execution/plan.rs@QueryPlanExecutionResult\r\n```\r\n\r\nThe execution pipeline now returns `QueryPlanExecutionResult` - an enum that can be either a single response or a stream:\r\n\r\n```rust\r\npub enum QueryPlanExecutionResult {\r\n Single(PlanExecutionOutput),\r\n Stream(PlanSubscriptionOutput),\r\n}\r\n```\r\n\r\nThis separation allows the query planner and executor to operate independently from transport concerns, making future improvements (like connection repair and silent retries) easier to implement.\r\n\r\n# Owned query plan execution context values for streaming\r\n\r\n```\r\nlib/executor/src/execution/plan.rs\r\n```\r\n\r\nSubscriptions require long-lived contexts that outlive request lifetimes, so we clone all of the necessary context values:\r\n\r\n- `Arc`-wrapped shared data\r\n- Enables entity resolution to happen independently for each subscription event\r\n- Processes remaining plan nodes after the subscription fetch\r\n\r\n## Avoid deep clones of shared data on every subscription setup\r\n\r\n`QueryPlanExecutionOpts` received `executors`, `projection_plan`, `headers_plan`, and `schema_metadata` as borrowed references. The subscription stream closure requires `'static` data, so these were deep-cloned into `Arc` on every subscription setup:\r\n\r\n```rust\r\nlet executors: Arc<SubgraphExecutorMap> = opts.executors.clone().into();\r\nlet projection_plan: Arc<Vec<FieldProjectionPlan>> = opts.projection_plan.clone().into();\r\nlet headers_plan: Arc<HeaderRulesPlan> = opts.headers_plan.clone().into();\r\nlet schema_metadata: Arc<SchemaMetadata> = Arc::new(opts.introspection_context.metadata.clone());\r\n```\r\n\r\n`SubgraphExecutorMap` contains multiple `DashMap`s, making its clone particularly expensive.\r\n\r\nWrapped `SupergraphData.subgraph_executor_map`, `SupergraphData.metadata`, and `RouterSharedState.headers_plan` in `Arc` at the source and threaded them through `QueryPlanExecutionOpts` and `IntrospectionContext` as `Arc` instead of borrowed references. `projection_plan` was already `Arc` at the source but was dereffed to a borrow before reaching `QueryPlanExecutionOpts`, now passed as `Arc` directly. The subscription path now does cheap ref-count bumps. The query/mutation path just derefs.\r\n\r\n# Subscription handlers\r\n\r\nThe router respects the client's `Accept` header to determine response format:\r\n\r\n- `text/event-stream` → SSE responses\r\n- `multipart/mixed` → Incremental Delivery over HTTP\r\n- `multipart/mixed;subscriptionSpec=\"1.0\"` → Apollo multipart HTTP\r\n- Returns `406 Not Acceptable` if subscription is requested over unsupported transport\r\n- Handles errors by emitting an error event and completing the stream\r\n- Heartbeats every 10 seconds (except for incremental delivery, doesn't have it)\r\n- Of course protocols used between router and subgraphs and clients can be different\r\n\r\nSame behavior is expected when communicating with subgraphs.\r\n\r\n## SSE\r\n\r\n```\r\nlib/executor/src/executors/sse.rs\r\n```\r\n\r\nImplements the [GraphQL over SSE spec distinct connection mode](https://github.com/enisdenjo/graphql-sse/blob/918b9d414a4f938c715a8f8bc13184c0849eac50/PROTOCOL.md#distinct-connections-mode).\r\n\r\n## Multipart protocol\r\n\r\n```\r\nlib/executor/src/executors/multipart_subscribe.rs\r\n```\r\n\r\nImplements [Apollo's multipart HTTP spec](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol) and [GraphQL's Incremental Delivery over HTTP RFC](https://github.com/graphql/graphql-over-http/blob/d312e43384006fa323b918d49cfd9fbd76ac1257/rfcs/IncrementalDelivery.md).\r\n\r\n# WebSockets\r\n\r\nRead stacked PR #738\r\n\r\n## Plugin system\r\n\r\nThe WS server assembles a synthetic http request for every ws operation message allowing the plugin system to run as usual for the supported execution paths. (No subscriptions because the plugin system generally does not support subscriptions - yet).\r\n\r\n# HTTP Callbacks\r\n\r\nRead stacked PR #831 \r\n\r\n# Entity resolution\r\n\r\n```\r\nlib/executor/src/execution/plan.rs@execute_query_plan\r\n```\r\n\r\nWhen a subscription emits data that references entities from other subgraphs, the router:\r\n\r\n1. Receives subscription event from primary subgraph\r\n2. Executes remaining plan nodes (flatten, fetch, etc.) to populate missing fields\r\n3. Projects the complete response\r\n4. Streams the full response to client\r\n\r\n# Subscription node in query plan\r\n\r\nIn PlanNode::from_fetch_step in `plan_nodes.rs`, when building a node from a fetch step, if the step has no response path and its operation kind is Subscription, it now directly produces a PlanNode::Subscription instead of PlanNode::Fetch.\r\n\r\n# Subgraph executor subscribe method\r\n\r\n```\r\nlib/executor/src/executors/common.rs@subscribe\r\n```\r\n\r\nThe HTTP executor gains a `subscribe()` method that:\r\n\r\n- Negotiates content-type (prefers multipart, falls back to SSE)\r\n- Establishes long-lived connections to subgraphs\r\n- Returns a `BoxStream<HttpExecutionResponse>` for downstream processing\r\n\r\n# Configure to only enable/disable subscriptions\r\n\r\nThe supported subscription protocols in this PR are inherintly HTTP and do not need a \"protocol\" configuration option. Hive Router will send an accept header listing all supported protocols for subscriptions over HTTP and the subgraph is free to choose whichever one it supports.\r\n\r\nWhether we really _want_ to limit specific protocols is up to discussion but objectively there is no benefit since they're all streamed HTTP.\r\n\r\nHence, you can only:\r\n\r\n```yaml\r\nsupergraph:\r\n source: file\r\n path: supergraph.graphql\r\nsubscriptions:\r\n enabled: true\r\n```\r\n\r\nP.S. if the subscriptions are disabled, the router will respond with when receiving a subscription request:\r\n\r\n```\r\nHTTP/1.1 415 Unsupported Media Type\r\nContent-Type: application/json\r\n\r\n{\"errors\":[{\"message\":\"Subscriptions are not supported\",\"extensions\":{\"code\":\"SUBSCRIPTIONS_NOT_SUPPORT\"}}]}\r\n```\r\n\r\n# No Silent Retries & Upstream Connection Repair\r\n\r\nMost GraphQL subgraph implementations are stateless for subscriptions and have no concept of \"continuing\" from where they left off after a connection loss. Implementing retry logic on the router side would create false expectations - users would assume all events are delivered, but some would be lost when the subgraph creates a fresh subscription.\r\n\r\nThis is fundamentally why the EDFS (Event-Driven Federated Subscriptions) and callback protocols exist. To avoid misleading behavior and keep the PR focused on the core functionality, connection repair is not implemented.\r\n\r\n# TODO\r\n\r\n- [x] [documentation](https://github.com/graphql-hive/console/pull/7472)\r\n - [ ] now with WS and HTTP callbacks\r\n - [ ] dont forget to document that WS assembles a synthetic http request \r\n- [ ] changesets\r\n- [x] e2e tests for error handling\r\n- [x] e2e tests for header propagation\r\n- [x] use details from the actual client request for entity resolution towards subgraphs of events\r\n- [x] test subgraph disconnect propagation\r\n- [ ] use apollo's multipart http payload for transport erros\r\n- [ ] ~~test client disconnect propagation~~\r\n - hard to test, would need changing the source code because async being state machines allows it just to stop execution in any suspended state\r\n- [ ] usage reporting (bin/router/src/pipeline/mod.rs#305)\r\n- [ ] performance considerations\r\n- [x] entity resolution failures dont end the stream, should they? a subgraph might recover, usually you dont end the whole stream. ~~**they should not end the stream**~~ actually they SHOULD because the router might not guarantee the subgraph will recover and the client might ignore the error keeping the gateway in an infinite error loop wasting resources\r\n- [ ] subscriptions deduplication (same subs same selection sets same headers)\r\n- [ ] reloading the supergraph notifies the subscribers\r\n- [ ] limits, things like client connection or event queue\r\n- [x] configure enable/disable subscriptions\r\n- [ ] ~~configure accepted protocols~~\r\n - no need, all these here fall under \"http\" protocols and can be negotiated over http. read description\r\n- [x] stream errors when accepting only streaming content types\r\n- [ ] validate config allowing only one subscriptions transport\r\n- [ ] plugin system\r\n- [ ] remove ntex patch https://github.com/ntex-rs/ntex/pull/790 and after main is rebased because there are some other necessary refactors for latest ntex v3",
"changed_files": 59,
"closed_at": null,
"comments": 3,
"comments_url": "https://api.github.com/repos/graphql-hive/router/issues/620/comments",
"commits": 200,
"commits_url": "https://api.github.com/repos/graphql-hive/router/pulls/620/commits",
"created_at": "2025-12-13T05:59:56Z",
"deletions": 341,
"diff_url": "https://github.com/graphql-hive/router/pull/620.diff",
"draft": false,
"head": {
"label": "graphql-hive:not-kamil-subs",
"ref": "not-kamil-subs",
"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": 8,
"forks_count": 8,
"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": true,
"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": 58,
"open_issues_count": 58,
"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-03-23T16:07:10Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 5895,
"squash_merge_commit_message": "PR_BODY",
"squash_merge_commit_title": "PR_TITLE",
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 78,
"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-03-21T17:31:04Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"use_squash_pr_title_as_default": true,
"visibility": "public",
"watchers": 78,
"watchers_count": 78,
"web_commit_signoff_required": false
},
"sha": "6cb2e62d38d3570fce6efdee36f86d745ca8d31d",
"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/620",
"id": 3098392050,
"issue_url": "https://api.github.com/repos/graphql-hive/router/issues/620",
"labels": [],
"locked": false,
"maintainer_can_modify": false,
"merge_commit_sha": "bf93129b5b605d9e7603c79908f7b5c6ca603cef",
"mergeable": null,
"mergeable_state": "unknown",
"merged": false,
"merged_at": null,
"merged_by": null,
"milestone": null,
"node_id": "PR_kwDONSTNFM64rbXy",
"number": 620,
"patch_url": "https://github.com/graphql-hive/router/pull/620.patch",
"rebaseable": null,
"requested_reviewers": [
{
"avatar_url": "https://avatars.githubusercontent.com/u/3680083?v=4",
"events_url": "https://api.github.com/users/dotansimha/events{/privacy}",
"followers_url": "https://api.github.com/users/dotansimha/followers",
"following_url": "https://api.github.com/users/dotansimha/following{/other_user}",
"gists_url": "https://api.github.com/users/dotansimha/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/dotansimha",
"id": 3680083,
"login": "dotansimha",
"node_id": "MDQ6VXNlcjM2ODAwODM=",
"organizations_url": "https://api.github.com/users/dotansimha/orgs",
"received_events_url": "https://api.github.com/users/dotansimha/received_events",
"repos_url": "https://api.github.com/users/dotansimha/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/dotansimha/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/dotansimha/subscriptions",
"type": "User",
"url": "https://api.github.com/users/dotansimha",
"user_view_type": "public"
},
{
"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"
},
{
"avatar_url": "https://avatars.githubusercontent.com/u/20847995?v=4",
"events_url": "https://api.github.com/users/ardatan/events{/privacy}",
"followers_url": "https://api.github.com/users/ardatan/followers",
"following_url": "https://api.github.com/users/ardatan/following{/other_user}",
"gists_url": "https://api.github.com/users/ardatan/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/ardatan",
"id": 20847995,
"login": "ardatan",
"node_id": "MDQ6VXNlcjIwODQ3OTk1",
"organizations_url": "https://api.github.com/users/ardatan/orgs",
"received_events_url": "https://api.github.com/users/ardatan/received_events",
"repos_url": "https://api.github.com/users/ardatan/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/ardatan/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/ardatan/subscriptions",
"type": "User",
"url": "https://api.github.com/users/ardatan",
"user_view_type": "public"
}
],
"requested_teams": [],
"review_comment_url": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}",
"review_comments": 20,
"review_comments_url": "https://api.github.com/repos/graphql-hive/router/pulls/620/comments",
"state": "open",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/6cb2e62d38d3570fce6efdee36f86d745ca8d31d",
"title": "feat: Subscriptions",
"updated_at": "2026-03-23T16:07:12Z",
"url": "https://api.github.com/repos/graphql-hive/router/pulls/620",
"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": 8,
"forks_count": 8,
"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": true,
"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": 58,
"open_issues_count": 58,
"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-03-23T16:07:10Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 5895,
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 78,
"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-03-21T17:31:04Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"visibility": "public",
"watchers": 78,
"watchers_count": 78,
"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/620/merge",
"github_ref_name": "620/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": "23447240895",
"github_run_number": "2067",
"github_runner_arch": "X64",
"github_runner_environment": "github-hosted",
"github_runner_image_os": "ubuntu24",
"github_runner_image_version": "20260309.50.1",
"github_runner_name": "GitHub Actions 1000682180",
"github_runner_os": "Linux",
"github_runner_tracking_id": "github_8291d775-1d94-4e70-8613-a53b43c008df",
"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/620/merge",
"github_workflow_sha": "5b300c5ef067668ed53d0d7d90058088f3ba5976",
"platform": "linux/amd64"
}
}
},
"buildx.build.provenance/linux/arm64": {
"builder": {
"id": "https://github.com/graphql-hive/router/actions/runs/23447240895/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": "329e54034ce498f9c6b345044e8f530c6691f99e94a92446f68c0adf9baa8464"
}
}
],
"invocation": {
"configSource": {
"entryPoint": "router.Dockerfile"
},
"parameters": {
"frontend": "gateway.v0",
"args": {
"cmdline": "docker/dockerfile:1.22",
"label:org.opencontainers.image.created": "2026-03-23T16:19:58.482Z",
"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": "5b300c5ef067668ed53d0d7d90058088f3ba5976",
"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-620",
"source": "docker/dockerfile:1.22"
},
"locals": [
{
"name": "context"
},
{
"name": "dockerfile"
}
]
},
"environment": {
"github_actor": "enisdenjo",
"github_actor_id": "11807600",
"github_event_name": "pull_request",
"github_event_payload": {
"action": "synchronize",
"after": "6cb2e62d38d3570fce6efdee36f86d745ca8d31d",
"before": "f1e9e1e3386eeddc6f86966ec2ed2e3de189145d",
"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-03-11T16:47:15Z",
"website_url": "https://the-guild.dev/"
},
"number": 620,
"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/620/comments"
},
"commits": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/620/commits"
},
"html": {
"href": "https://github.com/graphql-hive/router/pull/620"
},
"issue": {
"href": "https://api.github.com/repos/graphql-hive/router/issues/620"
},
"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/620/comments"
},
"self": {
"href": "https://api.github.com/repos/graphql-hive/router/pulls/620"
},
"statuses": {
"href": "https://api.github.com/repos/graphql-hive/router/statuses/6cb2e62d38d3570fce6efdee36f86d745ca8d31d"
}
},
"active_lock_reason": null,
"additions": 8456,
"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": 8,
"forks_count": 8,
"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": true,
"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": 58,
"open_issues_count": 58,
"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-03-23T16:07:10Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 5895,
"squash_merge_commit_message": "PR_BODY",
"squash_merge_commit_title": "PR_TITLE",
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 78,
"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-03-21T17:31:04Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"use_squash_pr_title_as_default": true,
"visibility": "public",
"watchers": 78,
"watchers_count": 78,
"web_commit_signoff_required": false
},
"sha": "72cf0ec5dc15f3540814a753fb4871ea2107054c",
"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": "Closes #313 \r\nRef router-165\r\n\r\nImplement [SSE](https://github.com/enisdenjo/graphql-sse/blob/918b9d414a4f938c715a8f8bc13184c0849eac50/PROTOCOL.md#distinct-connections-mode), [Incremental Delivery over HTTP](https://github.com/graphql/graphql-over-http/blob/d312e43384006fa323b918d49cfd9fbd76ac1257/rfcs/IncrementalDelivery.md), [Apollo's Multipart HTTP](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol), [WebSocket](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) and [HTTP Callback](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/callback-protocol) specs for subscriptions when communicating with subgraphs or clients with entity resolution capabilities.\r\n\r\n\r\n# Streaming execution result\r\n\r\n```\r\nlib/executor/src/execution/plan.rs@QueryPlanExecutionResult\r\n```\r\n\r\nThe execution pipeline now returns `QueryPlanExecutionResult` - an enum that can be either a single response or a stream:\r\n\r\n```rust\r\npub enum QueryPlanExecutionResult {\r\n Single(PlanExecutionOutput),\r\n Stream(PlanSubscriptionOutput),\r\n}\r\n```\r\n\r\nThis separation allows the query planner and executor to operate independently from transport concerns, making future improvements (like connection repair and silent retries) easier to implement.\r\n\r\n# Owned query plan execution context values for streaming\r\n\r\n```\r\nlib/executor/src/execution/plan.rs\r\n```\r\n\r\nSubscriptions require long-lived contexts that outlive request lifetimes, so we clone all of the necessary context values:\r\n\r\n- `Arc`-wrapped shared data\r\n- Enables entity resolution to happen independently for each subscription event\r\n- Processes remaining plan nodes after the subscription fetch\r\n\r\n## Avoid deep clones of shared data on every subscription setup\r\n\r\n`QueryPlanExecutionOpts` received `executors`, `projection_plan`, `headers_plan`, and `schema_metadata` as borrowed references. The subscription stream closure requires `'static` data, so these were deep-cloned into `Arc` on every subscription setup:\r\n\r\n```rust\r\nlet executors: Arc<SubgraphExecutorMap> = opts.executors.clone().into();\r\nlet projection_plan: Arc<Vec<FieldProjectionPlan>> = opts.projection_plan.clone().into();\r\nlet headers_plan: Arc<HeaderRulesPlan> = opts.headers_plan.clone().into();\r\nlet schema_metadata: Arc<SchemaMetadata> = Arc::new(opts.introspection_context.metadata.clone());\r\n```\r\n\r\n`SubgraphExecutorMap` contains multiple `DashMap`s, making its clone particularly expensive.\r\n\r\nWrapped `SupergraphData.subgraph_executor_map`, `SupergraphData.metadata`, and `RouterSharedState.headers_plan` in `Arc` at the source and threaded them through `QueryPlanExecutionOpts` and `IntrospectionContext` as `Arc` instead of borrowed references. `projection_plan` was already `Arc` at the source but was dereffed to a borrow before reaching `QueryPlanExecutionOpts`, now passed as `Arc` directly. The subscription path now does cheap ref-count bumps. The query/mutation path just derefs.\r\n\r\n# Subscription handlers\r\n\r\nThe router respects the client's `Accept` header to determine response format:\r\n\r\n- `text/event-stream` → SSE responses\r\n- `multipart/mixed` → Incremental Delivery over HTTP\r\n- `multipart/mixed;subscriptionSpec=\"1.0\"` → Apollo multipart HTTP\r\n- Returns `406 Not Acceptable` if subscription is requested over unsupported transport\r\n- Handles errors by emitting an error event and completing the stream\r\n- Heartbeats every 10 seconds (except for incremental delivery, doesn't have it)\r\n- Of course protocols used between router and subgraphs and clients can be different\r\n\r\nSame behavior is expected when communicating with subgraphs.\r\n\r\n## SSE\r\n\r\n```\r\nlib/executor/src/executors/sse.rs\r\n```\r\n\r\nImplements the [GraphQL over SSE spec distinct connection mode](https://github.com/enisdenjo/graphql-sse/blob/918b9d414a4f938c715a8f8bc13184c0849eac50/PROTOCOL.md#distinct-connections-mode).\r\n\r\n## Multipart protocol\r\n\r\n```\r\nlib/executor/src/executors/multipart_subscribe.rs\r\n```\r\n\r\nImplements [Apollo's multipart HTTP spec](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol) and [GraphQL's Incremental Delivery over HTTP RFC](https://github.com/graphql/graphql-over-http/blob/d312e43384006fa323b918d49cfd9fbd76ac1257/rfcs/IncrementalDelivery.md).\r\n\r\n# WebSockets\r\n\r\nRead stacked PR #738\r\n\r\n## Plugin system\r\n\r\nThe WS server assembles a synthetic http request for every ws operation message allowing the plugin system to run as usual for the supported execution paths. (No subscriptions because the plugin system generally does not support subscriptions - yet).\r\n\r\n# HTTP Callbacks\r\n\r\nRead stacked PR #831 \r\n\r\n# Entity resolution\r\n\r\n```\r\nlib/executor/src/execution/plan.rs@execute_query_plan\r\n```\r\n\r\nWhen a subscription emits data that references entities from other subgraphs, the router:\r\n\r\n1. Receives subscription event from primary subgraph\r\n2. Executes remaining plan nodes (flatten, fetch, etc.) to populate missing fields\r\n3. Projects the complete response\r\n4. Streams the full response to client\r\n\r\n# Subscription node in query plan\r\n\r\nIn PlanNode::from_fetch_step in `plan_nodes.rs`, when building a node from a fetch step, if the step has no response path and its operation kind is Subscription, it now directly produces a PlanNode::Subscription instead of PlanNode::Fetch.\r\n\r\n# Subgraph executor subscribe method\r\n\r\n```\r\nlib/executor/src/executors/common.rs@subscribe\r\n```\r\n\r\nThe HTTP executor gains a `subscribe()` method that:\r\n\r\n- Negotiates content-type (prefers multipart, falls back to SSE)\r\n- Establishes long-lived connections to subgraphs\r\n- Returns a `BoxStream<HttpExecutionResponse>` for downstream processing\r\n\r\n# Configure to only enable/disable subscriptions\r\n\r\nThe supported subscription protocols in this PR are inherintly HTTP and do not need a \"protocol\" configuration option. Hive Router will send an accept header listing all supported protocols for subscriptions over HTTP and the subgraph is free to choose whichever one it supports.\r\n\r\nWhether we really _want_ to limit specific protocols is up to discussion but objectively there is no benefit since they're all streamed HTTP.\r\n\r\nHence, you can only:\r\n\r\n```yaml\r\nsupergraph:\r\n source: file\r\n path: supergraph.graphql\r\nsubscriptions:\r\n enabled: true\r\n```\r\n\r\nP.S. if the subscriptions are disabled, the router will respond with when receiving a subscription request:\r\n\r\n```\r\nHTTP/1.1 415 Unsupported Media Type\r\nContent-Type: application/json\r\n\r\n{\"errors\":[{\"message\":\"Subscriptions are not supported\",\"extensions\":{\"code\":\"SUBSCRIPTIONS_NOT_SUPPORT\"}}]}\r\n```\r\n\r\n# No Silent Retries & Upstream Connection Repair\r\n\r\nMost GraphQL subgraph implementations are stateless for subscriptions and have no concept of \"continuing\" from where they left off after a connection loss. Implementing retry logic on the router side would create false expectations - users would assume all events are delivered, but some would be lost when the subgraph creates a fresh subscription.\r\n\r\nThis is fundamentally why the EDFS (Event-Driven Federated Subscriptions) and callback protocols exist. To avoid misleading behavior and keep the PR focused on the core functionality, connection repair is not implemented.\r\n\r\n# TODO\r\n\r\n- [x] [documentation](https://github.com/graphql-hive/console/pull/7472)\r\n - [ ] now with WS and HTTP callbacks\r\n - [ ] dont forget to document that WS assembles a synthetic http request \r\n- [ ] changesets\r\n- [x] e2e tests for error handling\r\n- [x] e2e tests for header propagation\r\n- [x] use details from the actual client request for entity resolution towards subgraphs of events\r\n- [x] test subgraph disconnect propagation\r\n- [ ] use apollo's multipart http payload for transport erros\r\n- [ ] ~~test client disconnect propagation~~\r\n - hard to test, would need changing the source code because async being state machines allows it just to stop execution in any suspended state\r\n- [ ] usage reporting (bin/router/src/pipeline/mod.rs#305)\r\n- [ ] performance considerations\r\n- [x] entity resolution failures dont end the stream, should they? a subgraph might recover, usually you dont end the whole stream. ~~**they should not end the stream**~~ actually they SHOULD because the router might not guarantee the subgraph will recover and the client might ignore the error keeping the gateway in an infinite error loop wasting resources\r\n- [ ] subscriptions deduplication (same subs same selection sets same headers)\r\n- [ ] reloading the supergraph notifies the subscribers\r\n- [ ] limits, things like client connection or event queue\r\n- [x] configure enable/disable subscriptions\r\n- [ ] ~~configure accepted protocols~~\r\n - no need, all these here fall under \"http\" protocols and can be negotiated over http. read description\r\n- [x] stream errors when accepting only streaming content types\r\n- [ ] validate config allowing only one subscriptions transport\r\n- [ ] plugin system\r\n- [ ] remove ntex patch https://github.com/ntex-rs/ntex/pull/790 and after main is rebased because there are some other necessary refactors for latest ntex v3",
"changed_files": 59,
"closed_at": null,
"comments": 3,
"comments_url": "https://api.github.com/repos/graphql-hive/router/issues/620/comments",
"commits": 200,
"commits_url": "https://api.github.com/repos/graphql-hive/router/pulls/620/commits",
"created_at": "2025-12-13T05:59:56Z",
"deletions": 341,
"diff_url": "https://github.com/graphql-hive/router/pull/620.diff",
"draft": false,
"head": {
"label": "graphql-hive:not-kamil-subs",
"ref": "not-kamil-subs",
"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": 8,
"forks_count": 8,
"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": true,
"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": 58,
"open_issues_count": 58,
"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-03-23T16:07:10Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 5895,
"squash_merge_commit_message": "PR_BODY",
"squash_merge_commit_title": "PR_TITLE",
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 78,
"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-03-21T17:31:04Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"use_squash_pr_title_as_default": true,
"visibility": "public",
"watchers": 78,
"watchers_count": 78,
"web_commit_signoff_required": false
},
"sha": "6cb2e62d38d3570fce6efdee36f86d745ca8d31d",
"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/620",
"id": 3098392050,
"issue_url": "https://api.github.com/repos/graphql-hive/router/issues/620",
"labels": [],
"locked": false,
"maintainer_can_modify": false,
"merge_commit_sha": "bf93129b5b605d9e7603c79908f7b5c6ca603cef",
"mergeable": null,
"mergeable_state": "unknown",
"merged": false,
"merged_at": null,
"merged_by": null,
"milestone": null,
"node_id": "PR_kwDONSTNFM64rbXy",
"number": 620,
"patch_url": "https://github.com/graphql-hive/router/pull/620.patch",
"rebaseable": null,
"requested_reviewers": [
{
"avatar_url": "https://avatars.githubusercontent.com/u/3680083?v=4",
"events_url": "https://api.github.com/users/dotansimha/events{/privacy}",
"followers_url": "https://api.github.com/users/dotansimha/followers",
"following_url": "https://api.github.com/users/dotansimha/following{/other_user}",
"gists_url": "https://api.github.com/users/dotansimha/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/dotansimha",
"id": 3680083,
"login": "dotansimha",
"node_id": "MDQ6VXNlcjM2ODAwODM=",
"organizations_url": "https://api.github.com/users/dotansimha/orgs",
"received_events_url": "https://api.github.com/users/dotansimha/received_events",
"repos_url": "https://api.github.com/users/dotansimha/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/dotansimha/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/dotansimha/subscriptions",
"type": "User",
"url": "https://api.github.com/users/dotansimha",
"user_view_type": "public"
},
{
"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"
},
{
"avatar_url": "https://avatars.githubusercontent.com/u/20847995?v=4",
"events_url": "https://api.github.com/users/ardatan/events{/privacy}",
"followers_url": "https://api.github.com/users/ardatan/followers",
"following_url": "https://api.github.com/users/ardatan/following{/other_user}",
"gists_url": "https://api.github.com/users/ardatan/gists{/gist_id}",
"gravatar_id": "",
"html_url": "https://github.com/ardatan",
"id": 20847995,
"login": "ardatan",
"node_id": "MDQ6VXNlcjIwODQ3OTk1",
"organizations_url": "https://api.github.com/users/ardatan/orgs",
"received_events_url": "https://api.github.com/users/ardatan/received_events",
"repos_url": "https://api.github.com/users/ardatan/repos",
"site_admin": false,
"starred_url": "https://api.github.com/users/ardatan/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/ardatan/subscriptions",
"type": "User",
"url": "https://api.github.com/users/ardatan",
"user_view_type": "public"
}
],
"requested_teams": [],
"review_comment_url": "https://api.github.com/repos/graphql-hive/router/pulls/comments{/number}",
"review_comments": 20,
"review_comments_url": "https://api.github.com/repos/graphql-hive/router/pulls/620/comments",
"state": "open",
"statuses_url": "https://api.github.com/repos/graphql-hive/router/statuses/6cb2e62d38d3570fce6efdee36f86d745ca8d31d",
"title": "feat: Subscriptions",
"updated_at": "2026-03-23T16:07:12Z",
"url": "https://api.github.com/repos/graphql-hive/router/pulls/620",
"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": 8,
"forks_count": 8,
"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": true,
"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": 58,
"open_issues_count": 58,
"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-03-23T16:07:10Z",
"releases_url": "https://api.github.com/repos/graphql-hive/router/releases{/id}",
"size": 5895,
"ssh_url": "git@github.com:graphql-hive/router.git",
"stargazers_count": 78,
"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-03-21T17:31:04Z",
"url": "https://api.github.com/repos/graphql-hive/router",
"visibility": "public",
"watchers": 78,
"watchers_count": 78,
"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/620/merge",
"github_ref_name": "620/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": "23447240895",
"github_run_number": "2067",
"github_runner_arch": "X64",
"github_runner_environment": "github-hosted",
"github_runner_image_os": "ubuntu24",
"github_runner_image_version": "20260309.50.1",
"github_runner_name": "GitHub Actions 1000682180",
"github_runner_os": "Linux",
"github_runner_tracking_id": "github_8291d775-1d94-4e70-8613-a53b43c008df",
"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/620/merge",
"github_workflow_sha": "5b300c5ef067668ed53d0d7d90058088f3ba5976",
"platform": "linux/amd64"
}
}
},
"buildx.build.ref": "builder-e7be889d-796a-4da0-a657-80971e1cbb89/builder-e7be889d-796a-4da0-a657-80971e1cbb890/520u3dkxvkvlmo5vx2ackvwi1",
"containerimage.descriptor": {
"mediaType": "application/vnd.oci.image.index.v1+json",
"digest": "sha256:b3f3e459270a807e8053a8d2c73f6854a0c343b750aa67212355b5274f964949",
"size": 1609
},
"containerimage.digest": "sha256:b3f3e459270a807e8053a8d2c73f6854a0c343b750aa67212355b5274f964949",
"image.name": "ghcr.io/graphql-hive/router:pr-620,ghcr.io/graphql-hive/router:sha-5b300c5"
} |
| pub struct SubscriptionNode { | ||
| pub primary: Box<PlanNode>, // Use Box to prevent size issues | ||
| // A subscription node can only really have a primary fetch node. | ||
| pub primary: FetchNode, |
There was a problem hiding this comment.
FetchNode is a concrete type and not an enum so I dropped the box because the compiler will know the size. Right?
Co-authored-by: Denis Badurina <denis@denelop.com>
Closes #313
Ref router-165
Implement SSE, Incremental Delivery over HTTP, Apollo's Multipart HTTP, WebSocket and HTTP Callback specs for subscriptions when communicating with subgraphs or clients with entity resolution capabilities.
Documentation
See PR in docs repo.
Streaming execution result
The execution pipeline returns
QueryPlanExecutionResult- an enum that can be either a single response or a stream:This separation allows the query planner and executor to operate independently from transport concerns, making future improvements (like connection repair and silent retries) easier to implement.
Owned query plan execution context values for streaming
Subscriptions require long-lived contexts that outlive request lifetimes. The subscription stream closure requires
'staticdata, soQueryPlanExecutionOptsfields captured by the stream areArc-wrapped so they can be cheaply cloned per event without deep-copying:operation_for_plan: Arc<OperationDefinition>variable_values: Arc<Option<HashMap<String, sonic_rs::Value>>>client_request: Arc<ClientRequestDetails>introspection_context: Arc<IntrospectionContext>executors,projection_plan,headers_planareArcat the call siteIntrospectionContextcarries no lifetime parameter -queryisOption<Arc<OperationDefinition>>andschemaisArc<Document>, both sourced from already-Arc-wrapped data inSupergraphData(planner.consumer_schema.document) andSchemaMetadata.SupergraphData.subgraph_executor_map,SupergraphData.metadata, andRouterSharedState.headers_planareArcat the source and are threaded throughQueryPlanExecutionOptsandIntrospectionContextasArcinstead of borrowed references.SubgraphExecutorMapcontains multipleDashMaps so keeping it behindArcis especially important. The subscription path does cheap ref-count bumps. The query/mutation path just derefs.This enables entity resolution to happen independently for each subscription event and allows remaining plan nodes to be processed after the subscription fetch.
Unified execution inner path for subscriptions
The post-fetch logic (plugin hooks, plan node execution, projection, error aggregation) lives in a private
execute_query_plan_with_data(data, opts)function shared by both the query/mutation path and the subscription stream. Each subscription event reconstructs anoptsfrom clonedArcs and callsexecute_query_plan_with_data, meaning subscription events go through the full execution pipeline including plugins.Error serialization helper
FailedExecutionResultis a serializable struct with aserialize()method (including a safe fallback if serialization itself fails) used in both the subscription stream and the pipeline error handler.Error messages in the subscription stream
When a subscription subgraph stream yields an error it is converted through
with_plan_context(which attaches the subgraph name) and thenFrom<PlanExecutionError> for GraphQLError, producing a message that includes the actual underlying error detail. For example:Subscription handlers
The router respects the client's
Acceptheader to determine response format:text/event-stream→ SSE responsesmultipart/mixed→ Incremental Delivery over HTTPmultipart/mixed;subscriptionSpec="1.0"→ Apollo multipart HTTP406 Not Acceptableif subscription is requested over unsupported transportSame behavior is expected when communicating with subgraphs.
SSE
Implements the GraphQL over SSE spec distinct connection mode.
Multipart protocol
Implements Apollo's multipart HTTP spec and GraphQL's Incremental Delivery over HTTP RFC.
WebSockets
Read stacked PR #738
Plugin system
The WS server assembles a synthetic http request for every ws operation message allowing the plugin system to run as usual for the supported execution paths. (No subscriptions because the plugin system generally does not support subscriptions - yet).
HTTP Callbacks
Read stacked PR #831
Entity resolution
When a subscription emits data that references entities from other subgraphs, the router:
Subscription node in query plan
In
PlanNode::from_fetch_stepinplan_nodes.rs, when building a node from a fetch step, if the step has no response path and its operation kind is Subscription, it directly produces aPlanNode::Subscriptioninstead ofPlanNode::Fetch.Subgraph executor subscribe method
The HTTP executor has a
subscribe()method that:BoxStream<HttpExecutionResponse>for downstream processingWebSocket executor
Request preparation (building a
SubscribePayloadand optionalConnectionInitPayloadfrom aSubgraphExecutionRequest) is handled by a sharedbuild_subscribe_payloadfunction.WsClient::subscribetakes a singleSubscribePayloadargument.SubscribePayloadderivesDefault.HTTP callback executor
The
subscriptionextension is injected by mutatingexecution_request.extensionsin place viaget_or_insert_default()before delegating to the sharedbuild_request_body.Configure to only enable/disable subscriptions
The supported subscription protocols are inherently HTTP and do not need a "protocol" configuration option. Hive Router sends an accept header listing all supported protocols for subscriptions over HTTP and the subgraph is free to choose whichever one it supports.
Whether we really want to limit specific protocols is up to discussion but objectively there is no benefit since they're all streamed HTTP.
Hence, you can only:
P.S. if the subscriptions are disabled, the router will respond when receiving a subscription request:
No Silent Retries & Upstream Connection Repair
Most GraphQL subgraph implementations are stateless for subscriptions and have no concept of "continuing" from where they left off after a connection loss. Implementing retry logic on the router side would create false expectations - users would assume all events are delivered, but some would be lost when the subgraph creates a fresh subscription.
This is fundamentally why the EDFS (Event-Driven Federated Subscriptions) and callback protocols exist. To avoid misleading behavior and keep the PR focused on the core functionality, connection repair is not implemented.
TODO
test client disconnect propagationthey should not end the streamactually they SHOULD because the router might not guarantee the subgraph will recover and the client might ignore the error keeping the gateway in an infinite error loop wasting resourcesconfigure accepted protocols