Skip to content

feat(router): add ConnectRPC transport for gRPC subgraphs#2808

Draft
fengyuwusong wants to merge 13 commits intowundergraph:mainfrom
fengyuwusong:feat/connectrpc-grpc-protocol-v2
Draft

feat(router): add ConnectRPC transport for gRPC subgraphs#2808
fengyuwusong wants to merge 13 commits intowundergraph:mainfrom
fengyuwusong:feat/connectrpc-grpc-protocol-v2

Conversation

@fengyuwusong
Copy link
Copy Markdown

@fengyuwusong fengyuwusong commented Apr 30, 2026

@coderabbitai summary

Summary

This PR adds first-class support for serving gRPC subgraphs over the ConnectRPC protocol (HTTP/1.1, Protobuf or JSON) in addition to the existing native-gRPC (HTTP/2) path. The transport is selected per subgraph through a new grpc_protocol configuration block on the router, so a federated graph can mix and match transports without changing the underlying subgraph implementation.

ConnectRPC is the right transport whenever end-to-end HTTP/2 to the subgraph is unavailable — for example when a corporate proxy or load balancer in front of the subgraph does not forward gRPC. A connect-go handler natively serves Connect, gRPC, and gRPC-Web on the same endpoint, so this change is purely additive on the subgraph side.

This supersedes #2665, which proposed the same feature without the meeting follow-ups (config naming, JSON schema, docs, demo & test coverage, control plane acceptance of http(s):// routing URLs, ConnectRPC example).

Related

What's changed

Router

  • New grpc_protocol configuration block on the router config:
grpc_protocol:
  default_protocol: connectrpc      # or "grpc" (default)
  default_encoding: proto           # or "json"
  subgraphs:
    products:
      protocol: grpc                # per-subgraph override
    employees:
      protocol: connectrpc
      encoding: json
  • router/pkg/grpcprotocol: validation, per-subgraph protocol/encoding resolution, and Connect-transport construction (BuildConnectTransports).
  • router/core/factoryresolver skips the gRPC connector for ConnectRPC subgraphs and threads the matching RPCTransport into the data source through graph_server.
  • Routing-URL normalisation: dns:///host:port, dns:host:port, plain host:port, and other gRPC name resolver schemes are converted to http://host:port for the Connect HTTP client.
  • All field naming follows the existing config conventions surfaced in the pre-merge review:
    • GRPCProtocolConfigGRPCProtocolConfiguration
    • DefaultDefaultProtocol
    • SubgraphGRPCProtocolConfigGRPCProtocolSubgraph
    • Full envDefault / env / envPrefix:"GRPC_PROTOCOL_" tags

Control plane

  • isValidGrpcSubgraphRoutingURL accepts both gRPC name resolver schemes and http(s):// URLs for GRPC_SERVICE subgraphs. The router decides the actual transport at request time, so the control plane no longer needs to commit to one or the other at registration. Applied to createFederatedSubgraph, updateSubgraph, and publishFederatedSubgraph.
  • Existing tests that asserted http(s) rejection are flipped to assert acceptance, and a new unit-test block for isValidGrpcSubgraphRoutingURL covers the http/https/case-insensitive/empty/delegation paths.

Demo

  • demo/pkg/subgraphs/projects migrates to buf generate with protoc-gen-go, protoc-gen-go-grpc, and protoc-gen-connect-go.
  • cmd/service/main.go exposes the projects subgraph through a Connect handler running over H2C on :4011, which serves Connect, gRPC, and gRPC-Web from the same endpoint.
  • Mechanically generated ProjectsConnectService adapter forwards every Connect call to the existing gRPC implementation (one method per RPC, 48 in total).

Documentation

  • docs-website/router/configuration.mdx: new "gRPC Subgraph Protocol" reference with environment-variable / YAML mapping table.
  • docs-website/router/gRPC/concepts.mdx: new "ConnectRPC Support" section.
  • docs-website/connect/grpc-services.mdx: short Transport Protocols paragraph linking back to the configuration reference.

Examples

  • examples/router-grpc-connect/: a minimal example mirroring router-simple/ that boots the projects subgraph and points the router at http://localhost:4011 with grpc_protocol.default_protocol: connectrpc.

Testing

  • router/pkg/grpcprotocol: 29 unit tests (config validate / resolve / build transports / normalizeConnectBaseURL).
  • router-tests/protocol/connectrpc_subgraph_test.go: 8 e2e cases covering queries, mutations, federation entity lookups, resolver-triggered field RPCs, plus per-subgraph protocol overrides and JSON encoding.
  • router-tests/protocol/grpc_subgraph_test.go: 78 existing cases pass — the new EnableConnectRPC testenv flag is opt-in, the gRPC path is unchanged.
  • router/pkg/config: testdata snapshots updated for the new GRPCProtocol field.
  • controlplane/test/subgraph/{create,update,publish}-subgraph.test.ts: flipped assertions to acceptance for http(s) URLs.

Migration / backward compatibility

  • grpc_protocol is omitted by default; existing graphs continue to use native gRPC with no behavioural change.
  • All YAML / env-tag names are net-new, no breaking renames of existing keys.

Checklist

  • I have discussed my proposed changes in an issue and have received approval to proceed.
  • I have followed the coding standards of the project.
  • Tests or benchmarks have been added or updated.
  • Documentation has been updated on https://github.com/wundergraph/docs-website.
  • I have read the Contributors Guide.

Open Source AI Manifesto

This project follows the principles of the Open Source AI Manifesto. Please ensure your contribution aligns with its principles.

fengyuwusong and others added 13 commits April 30, 2026 11:26
Add a `grpc_protocol` configuration section that allows users to choose
between native gRPC and ConnectRPC on a per-subgraph basis. Connect
subgraphs communicate over HTTP/1.1 with either Protobuf or JSON
encoding, bypassing the need for HTTP/2 end-to-end.

Key changes:
- New `GRPCProtocolConfig` in config with global defaults and
  per-subgraph overrides for protocol and encoding
- New `grpcprotocol` package with config validation, resolution,
  and transport builder
- Factory resolver checks Connect transports before gRPC connector
- Connect subgraphs skip native gRPC provider registration

Depends on: wundergraph/graphql-go-tools#1453
Closes: wundergraph#2664

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add grpcProtocol field to Config struct and WithGRPCProtocol option so
  the GRPCProtocol config from the YAML config is propagated into the
  graphServer (was always nil before, disabling ConnectRPC)
- Call grpcprotocol.Validate on startup to reject invalid protocol/
  encoding values early
- Pass properly-configured per-subgraph and default HTTP clients to
  BuildConnectTransports instead of nil, ensuring TLS and timeouts are
  respected for Connect subgraphs
- Wire WithGRPCProtocol(config.GRPCProtocol) in optionsFromResources
- Confirm Connect path in ResolveGraphqlFactory bypasses Disabled check,
  so the toGRPCConfiguration disabled flag is not an issue for Connect
  subgraphs (issue 2 is not a real bug)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…irective

- Include subgraphs with per-subgraph TLS (but no traffic shaping
  overrides) in Connect HTTP client map to avoid losing TLS config
- Comment out local replace directive for graphql-go-tools/v2;
  will be updated to published version once graphql-go-tools#1453 merges

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tions

Address feedback that the new gRPC transport-protocol configuration is not
discoverable through documentation alone. The renames make field intent
explicit and align tags with the rest of router/pkg/config:

- type GRPCProtocolConfig          -> GRPCProtocolConfiguration
- type SubgraphGRPCProtocolConfig  -> GRPCProtocolSubgraph
- field Default                    -> DefaultProtocol (yaml: default_protocol)
- add envDefault/env tags on default_protocol and default_encoding
- add envPrefix:"GRPC_PROTOCOL_" on the top-level grpc_protocol entry

Validation error keys are updated accordingly
(grpc_protocol.default -> grpc_protocol.default_protocol).
Add the grpc_protocol object (default_protocol, default_encoding, and
per-subgraph overrides) to router/pkg/config/config.schema.json so that
editor and CI validators recognize the new configuration. Update
testdata snapshots to reflect the new field on the Config struct.
Add a "gRPC Subgraph Protocol" section to the router configuration
reference covering default_protocol, default_encoding, and per-subgraph
overrides with environment variable / YAML mappings.

Add a brief "ConnectRPC Support" section to the gRPC concepts page that
explains when ConnectRPC is useful (no end-to-end HTTP/2) and links into
the configuration reference.
Migrate the projects standalone subgraph to a `buf generate`-based code
generation pipeline that produces three things from the same .proto:

  - Message structs (protoc-gen-go)
  - gRPC server/client (protoc-gen-go-grpc)
  - ConnectRPC handler/client (protoc-gen-connect-go)

The Connect handler natively serves Connect, gRPC, and gRPC-Web on the
same HTTP endpoint, so this lets the demo subgraph be reached over
either transport from the router based on the new `grpc_protocol`
configuration block.

Notable changes:

- Add buf.yaml and buf.gen.yaml. The Connect plugin needs Mservice.proto=
  to point at .../generated because the proto declares the bare module
  path as its go_package.
- Regenerate service.pb.go and service_grpc.pb.go through buf so the
  whole subgraph stays consistent with one generator pipeline.
- Add `make projects-buf-generate` target in demo/Makefile.
- Replace the standalone gRPC server in cmd/service/main.go with a
  ConnectRPC handler running over H2C on :4011. The handler accepts
  Connect, gRPC, and gRPC-Web traffic, so existing gRPC clients keep
  working unchanged.
- Add ProjectsConnectService (src/service/service_connect.go), a thin
  adapter that forwards every Connect call to the existing
  *ProjectsService gRPC implementation. Generated mechanically from the
  gRPC method signatures so it covers all 48 RPCs in one shot.
Add a new connectrpc_subgraph_test.go that drives the demo projects
subgraph through the router using the ConnectRPC protocol. The tests
mirror the four categories called out in the gRPC subgraph review:
plain queries, mutations, federation entity lookups, and resolver-
triggered field RPCs, plus protocol/encoding overrides via the
`grpc_protocol` configuration block.

To make the test possible:

- testenv gains an `EnableConnectRPC` flag that swaps the projects
  *grpc.Server for a ConnectRPC handler running over H2C. Existing
  EnableGRPC behaviour is unchanged when the flag is left false, so the
  78 existing TestGRPCSubgraph cases keep passing as a regression check.
- grpcprotocol.BuildConnectTransports now normalises the routing URL
  before handing it to the Connect HTTP client. URLs declared with the
  gRPC name resolver scheme (dns:///host:port) or as a bare host:port -
  the formats produced by the testenv replacements and config templates -
  are turned into http://host:port; existing http(s):// URLs pass through.
The Cosmo Connect gRPC Services overview page now points readers at the
new `grpc_protocol` configuration block so they discover from the entry
page that a gRPC subgraph can be reached over either native gRPC or
ConnectRPC. The configuration reference and the gRPC concepts page hold
the detailed schema and examples.
The routing URL validator on createFederatedSubgraph, updateSubgraph
and publishFederatedSubgraph rejected anything that did not match a
gRPC name resolver scheme (dns://, unix://, vsock://, ipv4://, ipv6://).
That blocks ConnectRPC subgraphs, which serve over plain HTTP/1.1 and
need an http(s):// routing URL even though their subgraph type is
GRPC_SERVICE.

Add isValidGrpcSubgraphRoutingURL that accepts either form:

  - any URL recognised by the existing isValidGrpcNamingScheme
    (preserves the historical strict gRPC validation), or
  - any well-formed http:// or https:// URL.

The router decides at request time which transport to use via the new
`grpc_protocol` configuration block, so the control plane does not
need to commit to one or the other at registration time. The error
message is updated to mention both options.
Three integration tests previously asserted that an http:// or https://
routing URL was rejected for GRPC_SERVICE subgraphs. Now that the
control plane accepts those URLs (so ConnectRPC subgraphs can register
their HTTP endpoint directly), invert the assertions:

  - createFederatedSubgraph with http(s) -> EnumStatusCode.OK
  - updateSubgraph with http(s)          -> EnumStatusCode.OK
  - publishFederatedSubgraph with http(s) -> EnumStatusCode.OK

Test names updated accordingly. The companion "Should allow ... with
gRPC naming scheme" tests remain unchanged so the gRPC path keeps the
same coverage.
…me handling

The helper that turns a federated routing URL into a base URL the
ConnectRPC HTTP client can dial only handled `http(s)://`, `dns:///` and
the bare `host:port` form. Routing URLs registered with the colon-only
forms (`dns:host:port`, `ipv4:host:port`, etc.) ended up as
`http://dns:host:port`, which would never resolve.

Rewrite the helper to recognise the full set of gRPC name resolver
schemes accepted by the control plane (dns, ipv4, ipv6, vsock, unix,
unix-abstract, passthrough) and add a table-driven test
TestNormalizeConnectBaseURL covering pass-through, the three dns shapes,
ipv4, unix, and the empty input edge case.
New examples/router-grpc-connect/ mirrors the existing router-simple
layout (config.yaml + graph.yaml + start.sh + README) but is wired up
to the demo projects gRPC subgraph reached over ConnectRPC.

The example covers the three pieces a user needs to put together to use
the new transport:

  - Router config with `grpc_protocol.default_protocol: connectrpc` plus
    a per-subgraph override snippet for mixed graphs.
  - Federation graph.yaml with an http:// routing URL for the gRPC
    subgraph and references to its proto/mapping/SDL files.
  - start.sh that boots the standalone projects subgraph (Connect handler
    over H2C on :4011), composes the supergraph, and starts the router.

The README also shows the matching `wgc grpc-service create
--routing-url http://...` invocation now that the control plane accepts
http(s) URLs for GRPC_SERVICE subgraphs.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

Walkthrough

This PR introduces ConnectRPC protocol support to the Cosmo Router, enabling gRPC subgraphs to communicate via HTTP/1.1 with Protobuf or JSON encoding in addition to native gRPC. Changes include validation updates for HTTP(S) routing URLs, configuration schema additions, transport builder implementation, test infrastructure, documentation, and a complete example demonstrating ConnectRPC integration.

Changes

Cohort / File(s) Summary
Controlplane Routing URL Validation
controlplane/src/core/bufservices/subgraph/createFederatedSubgraph.ts, publishFederatedSubgraph.ts, updateSubgraph.ts
Updated GRPC_SERVICE subgraph routing URL validation to accept both HTTP(S) URLs (http://host:port) for ConnectRPC and gRPC naming schemes (dns:///host:port), replacing strict gRPC naming-scheme-only checks with isValidGrpcSubgraphRoutingURL.
Controlplane Routing URL Utility
controlplane/src/core/util.ts, controlplane/src/core/util.test.ts
Added new exported validator isValidGrpcSubgraphRoutingURL that accepts HTTP(S) URLs via URL constructor validation and delegates non-HTTP(S) inputs to gRPC naming scheme validation; includes comprehensive test coverage.
Controlplane Subgraph Tests
controlplane/test/subgraph/create-subgraph.test.ts, publish-subgraph.test.ts, update-subgraph.test.ts
Updated test expectations to treat HTTP and HTTPS routing URLs as valid for GRPC service subgraphs, replacing error assertions with success checks and updating test descriptions to reference ConnectRPC behavior.
Demo Project Build Configuration
demo/Makefile, demo/pkg/subgraphs/projects/buf.yaml, demo/pkg/subgraphs/projects/buf.gen.yaml
Added Buf protobuf generation configuration and Makefile target (projects-buf-generate) to regenerate projects subgraph artifacts for standard Go protobuf, gRPC stubs, and ConnectRPC handlers with source-relative paths.
Demo Service Implementation
demo/pkg/subgraphs/projects/cmd/service/main.go, demo/pkg/subgraphs/projects/go.mod
Migrated service from native gRPC server to ConnectRPC HTTP handler over H2C, rewrote unary interceptors to Connect-style with panic recovery and error normalization, updated server lifecycle to http.Server, and added dependencies on connectrpc.com/connect and updated google.golang.org/protobuf.
Demo Service ConnectRPC Adapter
demo/pkg/subgraphs/projects/src/service/service_connect.go
Added auto-generated ConnectRPC service handler adapter (ProjectsConnectService) implementing projectsconnect.ProjectsServiceHandler interface with methods forwarding connect.Request/connect.Response to underlying gRPC service implementation.
Documentation—ConnectRPC Protocols
docs-website/connect/grpc-services.mdx, docs-website/router/gRPC/concepts.mdx, docs-website/router/configuration.mdx
Added documentation explaining native gRPC vs ConnectRPC transport options, HTTP/2 vs HTTP/1.1 characteristics, configuration via grpc_protocol settings, and per-subgraph protocol/encoding overrides with examples.
Example Project—ConnectRPC Setup
examples/router-grpc-connect/README.md, config.yaml, graph.yaml, start.sh
Introduced complete example demonstrating router with ConnectRPC subgraph: includes setup instructions, YAML configurations with connectrpc default protocol and proto encoding, graph definition with http:// routing URL, and bootstrap script orchestrating build and service registration.
Router Configuration Types
router/pkg/config/config.go, router/pkg/config/config.schema.json, router/core/router_config.go, router/core/router.go
Added GRPCProtocolConfiguration and GRPCProtocolSubgraph types supporting global default_protocol/default_encoding and per-subgraph overrides; extended Config struct with optional grpcProtocol field; added router option WithGRPCProtocol; updated JSON schema with enum constraints and defaults.
Router Protocol Support Module
router/pkg/grpcprotocol/config.go, config_test.go, transport_builder.go, transport_builder_test.go
New grpcprotocol package implementing protocol/encoding validation, fallback resolution to defaults, and BuildConnectTransports function that produces per-subgraph RPCTransport maps for ConnectRPC subgraphs; includes URL normalization from gRPC naming schemes to http:// format and encoding mapping (json → ConnectJSON).
Router Core Integration
router/core/executor.go, router/core/factoryresolver.go, router/core/graph_server.go, router/core/supervisor_instance.go
Integrated ConnectRPC support into executor builder (added connectTransports field), factory resolver (prioritizes Connect factories via early lookup), graph server (validates/stores protocol config, builds Connect transports, excludes ConnectRPC subgraphs from native gRPC connector), and supervisor (passes protocol config via router option).
Router Test Infrastructure
router-tests/testenv/testenv.go, router-tests/protocol/connectrpc_subgraph_test.go
Added EnableConnectRPC test configuration flag and makeSafeConnectRPCServer helper to run projects subgraph over HTTP/2 cleartext; added comprehensive integration test (TestConnectRPCSubgraph) exercising queries, mutations, federation lookups, resolvers, and encoding overrides with parallel subtests.
Test Data
router/pkg/config/testdata/config_defaults.json, config_full.json
Updated default test configuration files to include new GRPCProtocol field set to null.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR implements all primary coding objectives from #2664: grpc_protocol config with defaults/overrides, RPCTransport abstraction integration, control plane routing URL validation updates, transport builder for ConnectRPC, factory resolver changes to support Connect, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly aligned with the linked issue #2664 objectives: protocol/encoding configuration, transport builders, control plane updates, demo service migration to ConnectRPC, examples, documentation, and tests. No unrelated changes detected.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding ConnectRPC transport support for gRPC subgraphs, which is the primary focus across multiple router, control plane, demo, and test modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@fengyuwusong
Copy link
Copy Markdown
Author

CC @biubiubiuboomboomboom

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
demo/pkg/subgraphs/projects/go.mod (1)

33-39: ⚠️ Potential issue | 🟠 Major

Pin go.opentelemetry.io/otel/sdk to v1.40.0 or newer in this module.

Line 38 has go.opentelemetry.io/otel/sdk v1.28.0, which is vulnerable to GHSA-9h8m-3fm2-qjrq. Add an explicit version constraint to at least v1.40.0.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/pkg/subgraphs/projects/go.mod` around lines 33 - 39, Update the go.mod
entry for the OpenTelemetry SDK by pinning the module
go.opentelemetry.io/otel/sdk to at least v1.40.0 (replace the current v1.28.0
with v1.40.0 or newer) so the vulnerable version is not used; locate the go.mod
dependency line mentioning go.opentelemetry.io/otel/sdk and change its version
token accordingly, then run `go mod tidy` to reconcile dependencies.
🧹 Nitpick comments (3)
examples/router-grpc-connect/start.sh (1)

2-2: ⚡ Quick win

Use stricter shell mode for safer failures.

set -e misses unset-variable and pipeline failure cases. Using strict mode makes this bootstrap script more predictable in local/dev environments.

Proposed change
-set -e
+set -euo pipefail
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/router-grpc-connect/start.sh` at line 2, Replace the loose "set -e"
in start.sh with stricter shell options: enable -euo pipefail and set a safe IFS
(e.g., IFS=$'\n\t') so the script fails on unset variables and on pipeline
errors; update the top of the script where "set -e" appears to use these
stricter options to make bootstrap behavior deterministic in local/dev runs.
router-tests/testenv/testenv.go (1)

641-645: ⚡ Quick win

EnableConnectRPC currently bypasses the existing project interceptor hook.

The gRPC branch preserves cfg.Subgraphs.Projects.GRPCInterceptor, but the ConnectRPC branch ignores it entirely. That makes EnableConnectRPC silently change server behavior for interceptor-driven tests. Please either thread the hook through the Connect setup or fail fast when both options are set.

Possible fail-fast guard until Connect-side interception is supported
 if cfg.EnableGRPC {
 	if cfg.EnableConnectRPC {
+		if cfg.Subgraphs.Projects.GRPCInterceptor != nil {
+			return nil, errors.New("projects GRPCInterceptor is not supported with EnableConnectRPC")
+		}
 		connectHTTPServer, endpoint = makeSafeConnectRPCServer(t)
 	} else {
 		projectServer, endpoint = makeSafeGRPCServer(t, &projects.ProjectsService_ServiceDesc, &service.ProjectsService{}, cfg.Subgraphs.Projects.GRPCInterceptor)
 	}
 }

Also applies to: 1094-1098

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router-tests/testenv/testenv.go` around lines 641 - 645, The ConnectRPC
branch ignores the project-level gRPC interceptor
(cfg.Subgraphs.Projects.GRPCInterceptor) while the gRPC branch preserves it;
update makeSafeConnectRPCServer or the setup logic so the
Projects.GRPCInterceptor is either passed into the Connect server creation
(thread the hook through makeSafeConnectRPCServer and use it when constructing
the Connect handlers for ProjectsService) or add a fail-fast check where
cfg.EnableConnectRPC is true and cfg.Subgraphs.Projects.GRPCInterceptor is
non-nil (return an explicit error or t.Fatalf) to prevent silent behavioral
changes; adjust the same logic where projectServer/connectHTTPServer and
endpoint are assigned (the code around makeSafeConnectRPCServer and
makeSafeGRPCServer) and apply the same fix to the other occurrence referenced.
router-tests/protocol/connectrpc_subgraph_test.go (1)

22-181: ⚡ Quick win

These parallel subtests are still sharing mutable demo state.

Lines 58-60 already call out that updateProjectStatus can change data seen by sibling cases. Dropping the status assertion hides one symptom, but the suite is still order-dependent as long as every subtest runs with t.Parallel() against the same in-memory store. Please serialize this suite or give each environment an isolated projects store.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router-tests/protocol/connectrpc_subgraph_test.go` around lines 22 - 181, The
subtests in TestConnectRPCSubgraph run in parallel while sharing a mutable
in-memory demo store (so updateProjectStatus can mutate state seen by siblings);
fix by either removing the per-subtest t.Parallel() calls (serialize the
subtests) or ensure each test gets an isolated projects store by changing the
testenv.Run invocation to supply a fresh store per run via testenv.Config (so
each testenv.Run creates an independent environment rather than sharing the demo
state); update references around TestConnectRPCSubgraph, the t.Parallel() calls,
testenv.Run and testenv.Config (or the code path that seeds the in-memory store)
accordingly so updateProjectStatus no longer causes cross-test interference.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@demo/pkg/subgraphs/projects/cmd/service/main.go`:
- Around line 37-46: The recoveryInterceptor currently logs and then returns raw
panic details to clients; change it so the deferred panic handler still logs the
recovered value (e.g., keep log.Printf("Recovered from panic: %v", r) or expand
to include stack trace) but replace the client-facing error produced by
connect.NewError/connect.CodeInternal with a generic message like "internal
server error" (do not include r or stack info). Update any other places in the
same interceptor (and similar handlers around the defer/err assignment) that
build client responses (e.g., calls to connect.NewError or status.Errorf) to use
the generic message while preserving detailed diagnostics only in server logs.

In `@docs-website/router/configuration.mdx`:
- Around line 2439-2456: The YAML example uses the same top-level key
grpc_protocol twice which is invalid/misleading; split the example into two
separate code blocks (or a CodeGroup of two snippets) so each example has its
own version: one block showing a single global grpc_protocol with
default_protocol: connectrpc and default_encoding: proto, and a second block
showing a different grpc_protocol example with default_protocol: grpc and a
subgraphs map (employees/products) — update the snippet surrounding the
grpc_protocol occurrences (search for grpc_protocol, default_protocol,
default_encoding, subgraphs) to implement the two distinct examples instead of
duplicate keys.

In `@docs-website/router/gRPC/concepts.mdx`:
- Around line 252-256: Replace the em dash in the sentence containing "is not
possible — for example when a proxy or load balancer in front of the subgraph
does not forward gRPC." with a period (or otherwise restructure into two
sentences) so it reads like: "...is not possible. For example, when a proxy or
load balancer in front of the subgraph does not forward gRPC." Update the MDX
paragraph that explains ConnectRPC accordingly to comply with the MDX docs
guideline to avoid em dashes.

In `@examples/router-grpc-connect/graph.yaml`:
- Line 20: The mappingFile value in graph.yaml currently points to a
non-existent path; update the mappingFile entry so it references the actual
relative location of mapping.json in the repository (replace the current
'../../demo/pkg/subgraphs/projects/mapping.json' value with the correct relative
path to the real mapping.json file) so the example config loads successfully;
look for the mapping.json file in the demo/pkg/subgraphs or demo directory tree
and correct the mappingFile key accordingly.

In `@router/pkg/grpcprotocol/transport_builder.go`:
- Around line 64-99: normalizeConnectBaseURL currently maps non-TCP schemes
(unix, unix-abstract, vsock, passthrough) into an http:// URL but the HTTP
client/transport still dials TCP, causing runtime failures; update the code path
that handles ConnectRPC to either reject these non-TCP schemes early or add a
proper custom dialer for them. Specifically, in the code that selects/uses
normalizeConnectBaseURL and constructs the *http.Client / http.Transport for
ConnectRPC, detect schemes returned by normalizeConnectBaseURL (or inspect the
original routingURL prefixes such as "unix:", "unix-abstract:", "vsock:") and
(a) return a validation error when the selected protocol is connectrpc and the
scheme is not a TCP/HTTP-compatible one, or (b) implement and attach a custom
DialContext/Dial function on the http.Transport that understands unix domain
sockets and vsock and performs the correct dial for "unix:" / "unix-abstract:" /
"vsock:" endpoints; keep references to normalizeConnectBaseURL, the
http.Transport construction, and any transport builder function so reviewers can
locate the change.

---

Outside diff comments:
In `@demo/pkg/subgraphs/projects/go.mod`:
- Around line 33-39: Update the go.mod entry for the OpenTelemetry SDK by
pinning the module go.opentelemetry.io/otel/sdk to at least v1.40.0 (replace the
current v1.28.0 with v1.40.0 or newer) so the vulnerable version is not used;
locate the go.mod dependency line mentioning go.opentelemetry.io/otel/sdk and
change its version token accordingly, then run `go mod tidy` to reconcile
dependencies.

---

Nitpick comments:
In `@examples/router-grpc-connect/start.sh`:
- Line 2: Replace the loose "set -e" in start.sh with stricter shell options:
enable -euo pipefail and set a safe IFS (e.g., IFS=$'\n\t') so the script fails
on unset variables and on pipeline errors; update the top of the script where
"set -e" appears to use these stricter options to make bootstrap behavior
deterministic in local/dev runs.

In `@router-tests/protocol/connectrpc_subgraph_test.go`:
- Around line 22-181: The subtests in TestConnectRPCSubgraph run in parallel
while sharing a mutable in-memory demo store (so updateProjectStatus can mutate
state seen by siblings); fix by either removing the per-subtest t.Parallel()
calls (serialize the subtests) or ensure each test gets an isolated projects
store by changing the testenv.Run invocation to supply a fresh store per run via
testenv.Config (so each testenv.Run creates an independent environment rather
than sharing the demo state); update references around TestConnectRPCSubgraph,
the t.Parallel() calls, testenv.Run and testenv.Config (or the code path that
seeds the in-memory store) accordingly so updateProjectStatus no longer causes
cross-test interference.

In `@router-tests/testenv/testenv.go`:
- Around line 641-645: The ConnectRPC branch ignores the project-level gRPC
interceptor (cfg.Subgraphs.Projects.GRPCInterceptor) while the gRPC branch
preserves it; update makeSafeConnectRPCServer or the setup logic so the
Projects.GRPCInterceptor is either passed into the Connect server creation
(thread the hook through makeSafeConnectRPCServer and use it when constructing
the Connect handlers for ProjectsService) or add a fail-fast check where
cfg.EnableConnectRPC is true and cfg.Subgraphs.Projects.GRPCInterceptor is
non-nil (return an explicit error or t.Fatalf) to prevent silent behavioral
changes; adjust the same logic where projectServer/connectHTTPServer and
endpoint are assigned (the code around makeSafeConnectRPCServer and
makeSafeGRPCServer) and apply the same fix to the other occurrence referenced.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d81d5e6-a69d-416d-a287-4d05e7184298

📥 Commits

Reviewing files that changed from the base of the PR and between 9780ce7 and 78ddc4c.

⛔ Files ignored due to path filters (4)
  • demo/pkg/subgraphs/projects/generated/projectsconnect/service.connect.go is excluded by !**/generated/**
  • demo/pkg/subgraphs/projects/generated/service.pb.go is excluded by !**/*.pb.go, !**/generated/**
  • demo/pkg/subgraphs/projects/generated/service_grpc.pb.go is excluded by !**/*.pb.go, !**/generated/**
  • demo/pkg/subgraphs/projects/go.sum is excluded by !**/*.sum
📒 Files selected for processing (37)
  • controlplane/src/core/bufservices/subgraph/createFederatedSubgraph.ts
  • controlplane/src/core/bufservices/subgraph/publishFederatedSubgraph.ts
  • controlplane/src/core/bufservices/subgraph/updateSubgraph.ts
  • controlplane/src/core/util.test.ts
  • controlplane/src/core/util.ts
  • controlplane/test/subgraph/create-subgraph.test.ts
  • controlplane/test/subgraph/publish-subgraph.test.ts
  • controlplane/test/subgraph/update-subgraph.test.ts
  • demo/Makefile
  • demo/pkg/subgraphs/projects/buf.gen.yaml
  • demo/pkg/subgraphs/projects/buf.yaml
  • demo/pkg/subgraphs/projects/cmd/service/main.go
  • demo/pkg/subgraphs/projects/go.mod
  • demo/pkg/subgraphs/projects/src/service/service_connect.go
  • docs-website/connect/grpc-services.mdx
  • docs-website/router/configuration.mdx
  • docs-website/router/gRPC/concepts.mdx
  • examples/router-grpc-connect/README.md
  • examples/router-grpc-connect/config.yaml
  • examples/router-grpc-connect/graph.yaml
  • examples/router-grpc-connect/start.sh
  • router-tests/protocol/connectrpc_subgraph_test.go
  • router-tests/testenv/testenv.go
  • router/core/executor.go
  • router/core/factoryresolver.go
  • router/core/graph_server.go
  • router/core/router.go
  • router/core/router_config.go
  • router/core/supervisor_instance.go
  • router/pkg/config/config.go
  • router/pkg/config/config.schema.json
  • router/pkg/config/testdata/config_defaults.json
  • router/pkg/config/testdata/config_full.json
  • router/pkg/grpcprotocol/config.go
  • router/pkg/grpcprotocol/config_test.go
  • router/pkg/grpcprotocol/transport_builder.go
  • router/pkg/grpcprotocol/transport_builder_test.go

Comment on lines +37 to +46
// recoveryInterceptor catches panics from RPC handlers so that one bad
// request cannot bring down the whole server.
func recoveryInterceptor() connect.UnaryInterceptorFunc {
return func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (resp connect.AnyResponse, err error) {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered from panic: %v", r)
err = connect.NewError(connect.CodeInternal, status.Errorf(codes.Internal, "panic: %v", r))
}
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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not expose panic/internal error details to clients.

Line 45 and Line 79 include raw panic/error values in client-facing responses. That leaks internals. Return a generic internal error message and keep specifics in logs.

Suggested hardening patch
 func recoveryInterceptor() connect.UnaryInterceptorFunc {
@@
 			defer func() {
 				if r := recover(); r != nil {
 					log.Printf("Recovered from panic: %v", r)
-					err = connect.NewError(connect.CodeInternal, status.Errorf(codes.Internal, "panic: %v", r))
+					err = connect.NewError(connect.CodeInternal, status.Error(codes.Internal, "internal server error"))
 				}
 			}()
@@
 func errorInterceptor() connect.UnaryInterceptorFunc {
@@
 			if err != nil {
 				var connErr *connect.Error
 				if !errors.As(err, &connErr) {
 					if _, ok := status.FromError(err); !ok {
-						err = connect.NewError(connect.CodeInternal, status.Errorf(codes.Internal, "internal server error: %v", err))
+						err = connect.NewError(connect.CodeInternal, status.Error(codes.Internal, "internal server error"))
 					}
 				}
 			}

Also applies to: 69-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@demo/pkg/subgraphs/projects/cmd/service/main.go` around lines 37 - 46, The
recoveryInterceptor currently logs and then returns raw panic details to
clients; change it so the deferred panic handler still logs the recovered value
(e.g., keep log.Printf("Recovered from panic: %v", r) or expand to include stack
trace) but replace the client-facing error produced by
connect.NewError/connect.CodeInternal with a generic message like "internal
server error" (do not include r or stack info). Update any other places in the
same interceptor (and similar handlers around the defer/err assignment) that
build client responses (e.g., calls to connect.NewError or status.Errorf) to use
the generic message while preserving detailed diagnostics only in server logs.

Comment on lines +2439 to +2456
```yaml config.yaml
version: "1"

# Switch all gRPC subgraphs to ConnectRPC over HTTP/1.1
grpc_protocol:
default_protocol: connectrpc
default_encoding: proto

# Or mix protocols on a per-subgraph basis
grpc_protocol:
default_protocol: grpc
subgraphs:
employees:
protocol: connectrpc
encoding: json
products:
protocol: grpc
```
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid duplicate grpc_protocol keys in one YAML example.

This snippet defines grpc_protocol twice in the same document. In YAML, the second key overrides the first, so copy-paste behavior is misleading.

Proposed change
-```yaml config.yaml
-version: "1"
-
-# Switch all gRPC subgraphs to ConnectRPC over HTTP/1.1
-grpc_protocol:
-  default_protocol: connectrpc
-  default_encoding: proto
-
-# Or mix protocols on a per-subgraph basis
-grpc_protocol:
-  default_protocol: grpc
-  subgraphs:
-    employees:
-      protocol: connectrpc
-      encoding: json
-    products:
-      protocol: grpc
-```
+<CodeGroup>
+  ```yaml All gRPC subgraphs use ConnectRPC
+  version: "1"
+  grpc_protocol:
+    default_protocol: connectrpc
+    default_encoding: proto
+  ```
+
+  ```yaml Mixed per-subgraph protocol
+  version: "1"
+  grpc_protocol:
+    default_protocol: grpc
+    subgraphs:
+      employees:
+        protocol: connectrpc
+        encoding: json
+      products:
+        protocol: grpc
+  ```
+</CodeGroup>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs-website/router/configuration.mdx` around lines 2439 - 2456, The YAML
example uses the same top-level key grpc_protocol twice which is
invalid/misleading; split the example into two separate code blocks (or a
CodeGroup of two snippets) so each example has its own version: one block
showing a single global grpc_protocol with default_protocol: connectrpc and
default_encoding: proto, and a second block showing a different grpc_protocol
example with default_protocol: grpc and a subgraphs map (employees/products) —
update the snippet surrounding the grpc_protocol occurrences (search for
grpc_protocol, default_protocol, default_encoding, subgraphs) to implement the
two distinct examples instead of duplicate keys.

Comment on lines +252 to +256
The router can talk to gRPC subgraphs over either native gRPC (HTTP/2 +
Protobuf) or [ConnectRPC](https://connectrpc.com/) (HTTP/1.1 with Protobuf
or JSON). ConnectRPC is useful when end-to-end HTTP/2 to the subgraph is
not possible — for example when a proxy or load balancer in front of the
subgraph does not forward gRPC.
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Replace the em dash in this paragraph.

Line 255 uses an em dash, which conflicts with the docs writing rules for MDX files.

Proposed change
-The router can talk to gRPC subgraphs over either native gRPC (HTTP/2 +
-Protobuf) or [ConnectRPC](https://connectrpc.com/) (HTTP/1.1 with Protobuf
-or JSON). ConnectRPC is useful when end-to-end HTTP/2 to the subgraph is
-not possible — for example when a proxy or load balancer in front of the
-subgraph does not forward gRPC.
+The router can talk to gRPC subgraphs over either native gRPC (HTTP/2 +
+Protobuf) or [ConnectRPC](https://connectrpc.com/) (HTTP/1.1 with Protobuf
+or JSON). ConnectRPC is useful when end-to-end HTTP/2 to the subgraph is
+not possible. For example, a proxy or load balancer in front of the
+subgraph may not forward gRPC.

As per coding guidelines: "Avoid em dashes. Use periods or restructure the sentence instead."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
The router can talk to gRPC subgraphs over either native gRPC (HTTP/2 +
Protobuf) or [ConnectRPC](https://connectrpc.com/) (HTTP/1.1 with Protobuf
or JSON). ConnectRPC is useful when end-to-end HTTP/2 to the subgraph is
not possible — for example when a proxy or load balancer in front of the
subgraph does not forward gRPC.
The router can talk to gRPC subgraphs over either native gRPC (HTTP/2 +
Protobuf) or [ConnectRPC](https://connectrpc.com/) (HTTP/1.1 with Protobuf
or JSON). ConnectRPC is useful when end-to-end HTTP/2 to the subgraph is
not possible. For example, a proxy or load balancer in front of the
subgraph may not forward gRPC.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs-website/router/gRPC/concepts.mdx` around lines 252 - 256, Replace the em
dash in the sentence containing "is not possible — for example when a proxy or
load balancer in front of the subgraph does not forward gRPC." with a period (or
otherwise restructure into two sentences) so it reads like: "...is not possible.
For example, when a proxy or load balancer in front of the subgraph does not
forward gRPC." Update the MDX paragraph that explains ConnectRPC accordingly to
comply with the MDX docs guideline to avoid em dashes.

file: ../../demo/pkg/subgraphs/projects/src/schema.graphql
grpc:
protoFile: ../../demo/pkg/subgraphs/projects/generated/service.proto
mappingFile: ../../demo/pkg/subgraphs/projects/mapping.json
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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix mappingFile path in the example config

Line 20 points to a non-existent file path for mapping.json; this example will fail as-is.

Suggested fix
-      mappingFile: ../../demo/pkg/subgraphs/projects/mapping.json
+      mappingFile: ../../demo/pkg/subgraphs/projects/generated/mapping.json
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mappingFile: ../../demo/pkg/subgraphs/projects/mapping.json
mappingFile: ../../demo/pkg/subgraphs/projects/generated/mapping.json
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/router-grpc-connect/graph.yaml` at line 20, The mappingFile value in
graph.yaml currently points to a non-existent path; update the mappingFile entry
so it references the actual relative location of mapping.json in the repository
(replace the current '../../demo/pkg/subgraphs/projects/mapping.json' value with
the correct relative path to the real mapping.json file) so the example config
loads successfully; look for the mapping.json file in the demo/pkg/subgraphs or
demo directory tree and correct the mappingFile key accordingly.

Comment on lines +64 to +99
// The set of supported source schemes mirrors isValidGrpcNamingScheme on
// the control plane (dns, ipv4, ipv6, vsock, unix, unix-abstract,
// passthrough). For schemes that wrap an authority component
// (`scheme://authority/endpoint`) the authority is dropped because Connect
// targets the endpoint directly.
func normalizeConnectBaseURL(routingURL string) string {
if routingURL == "" {
return routingURL
}
if strings.HasPrefix(routingURL, "http://") || strings.HasPrefix(routingURL, "https://") {
return routingURL
}

// Order matters: longer prefixes must be checked before their shorter
// substrings (`unix-abstract:` before `unix:`).
grpcSchemes := []string{"unix-abstract:", "passthrough:", "dns:", "ipv4:", "ipv6:", "vsock:", "unix:"}
for _, prefix := range grpcSchemes {
if !strings.HasPrefix(routingURL, prefix) {
continue
}
rest := routingURL[len(prefix):]
// `scheme://authority/endpoint` and `scheme:///endpoint` both end up
// with a leading `//`; strip the authority and the path separator so
// only the endpoint survives.
if strings.HasPrefix(rest, "//") {
after := rest[2:]
if i := strings.Index(after, "/"); i >= 0 {
rest = after[i+1:]
} else {
rest = after
}
}
return "http://" + rest
}
return "http://" + routingURL
}
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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Reject non-TCP resolver schemes for ConnectRPC.

normalizeConnectBaseURL rewrites unix:, unix-abstract:, and vsock: into plain http://... URLs, but the transport you build here is still a normal *http.Client. For example, unix:///tmp/grpc.sock becomes http://tmp/grpc.sock, which http.Transport will try to dial as TCP host tmp, not as a Unix socket. That means ConnectRPC subgraphs registered with these currently-advertised routing schemes will fail at runtime. Please either fail validation for non-HTTP/TCP schemes when connectrpc is selected, or add a custom dialer that actually supports those transports.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router/pkg/grpcprotocol/transport_builder.go` around lines 64 - 99,
normalizeConnectBaseURL currently maps non-TCP schemes (unix, unix-abstract,
vsock, passthrough) into an http:// URL but the HTTP client/transport still
dials TCP, causing runtime failures; update the code path that handles
ConnectRPC to either reject these non-TCP schemes early or add a proper custom
dialer for them. Specifically, in the code that selects/uses
normalizeConnectBaseURL and constructs the *http.Client / http.Transport for
ConnectRPC, detect schemes returned by normalizeConnectBaseURL (or inspect the
original routingURL prefixes such as "unix:", "unix-abstract:", "vsock:") and
(a) return a validation error when the selected protocol is connectrpc and the
scheme is not a TCP/HTTP-compatible one, or (b) implement and attach a custom
DialContext/Dial function on the http.Transport that understands unix domain
sockets and vsock and performs the correct dial for "unix:" / "unix-abstract:" /
"vsock:" endpoints; keep references to normalizeConnectBaseURL, the
http.Transport construction, and any transport builder function so reviewers can
locate the change.

@fengyuwusong fengyuwusong changed the title Feat/connectrpc grpc protocol v2 feat(router): add ConnectRPC transport for gRPC subgraphs Apr 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(router): Add ConnectRPC protocol support for gRPC subgraphs

1 participant