Component(s)
router
Component version
github.com/wundergraph/cosmo/router local build based on 5b323e7090c6
wgc version
N/A (not involved in the reproduction; router loads execution config from file)
controlplane version
N/A / local execution config
router version
dev (local build based on 5b323e7090c6)
What happened?
For gRPC datasources, an optional nested input object is serialized as an empty protobuf message even when the field is omitted from GraphQL variables, and also when it is explicitly set to null.
This changes request semantics for downstream services because:
field omitted
field: null
field: {}
all become effectively the same protobuf payload: an empty nested message.
Description
A minimal shape like this reproduces the problem:
type Mutation {
updateRule(input: UpdateRuleInput!): Rule!
}
type Rule {
id: ID!
}
input UpdateRuleInput {
id: ID!
filter: FilterInput
}
input FilterInput {
key: String
operator: String
}
The corresponding generated protobuf request contains a nested message field:
message UpdateRuleInput {
string id = 1;
FilterInput filter = 2;
}
message FilterInput {
google.protobuf.StringValue key = 1;
google.protobuf.StringValue operator = 2;
}
When the GraphQL variables omit filter, the compiled protobuf request still includes filter: {}.
The same happens when filter is explicitly set to null.
From local debugging, this appears to come from pkg/engine/datasource/grpc_datasource/compiler.go, where nested message fields in the default branch are always built and message.Set(...) is always called without an existence check.
Steps to Reproduce
- Start the router with a gRPC datasource whose input contains an optional nested message field like
filter: FilterInput.
- Send this request with the field omitted:
{
"query": "mutation UpdateRule($input: UpdateRuleInput!) { updateRule(input: $input) { id } }",
"variables": {
"input": {
"id": "r1"
}
}
}
- Inspect the compiled/proxied protobuf request.
- Repeat with
"filter": null.
Expected Result
- If the GraphQL field is omitted, the protobuf nested message should be omitted as well.
- If the GraphQL field is
null, the nested message should also be omitted, or at least preserve null semantics consistently.
- The router should not silently convert omitted/null nested input objects into
{}.
Actual Result
Both of these inputs:
and
{"input":{"id":"r1","filter":null}}
produce a protobuf payload equivalent to:
{"input":{"id":"r1","filter":{}}}
That makes downstream services unable to distinguish omitted vs null vs empty-object input.
Environment information
Environment
OS: macOS (Apple Silicon)
Package Manager: Go modules
Compiler(if manually compiled): go1.23.0
Router configuration
listen_addr: "127.0.0.1:18080"
dev_mode: true
plugins:
enabled: false
execution_config:
file:
path: "./router.execution.config.json"
watch: true
Router execution config
{
"kind": "GRAPHQL",
"customGraphql": {
"grpc": {
"mapping": {
"service": "RpcService"
},
"protoSchema": "message UpdateRuleInput { string id = 1; FilterInput filter = 2; } message FilterInput { google.protobuf.StringValue key = 1; google.protobuf.StringValue operator = 2; }"
}
}
}
Log output
No router panic is required to reproduce this.
The issue is visible in the compiled protobuf request: omitted/null nested input object becomes an empty message.
Additional context
Optional scalar wrappers already seem to do an existence check before serializing. The behavior difference only appears on ordinary nested message fields.
Component(s)
router
Component version
github.com/wundergraph/cosmo/routerlocal build based on5b323e7090c6wgc version
N/A (not involved in the reproduction; router loads execution config from file)
controlplane version
N/A / local execution config
router version
dev (local build based on
5b323e7090c6)What happened?
For gRPC datasources, an optional nested input object is serialized as an empty protobuf message even when the field is omitted from GraphQL variables, and also when it is explicitly set to
null.This changes request semantics for downstream services because:
field omittedfield: nullfield: {}all become effectively the same protobuf payload: an empty nested message.
Description
A minimal shape like this reproduces the problem:
The corresponding generated protobuf request contains a nested message field:
When the GraphQL variables omit
filter, the compiled protobuf request still includesfilter: {}.The same happens when
filteris explicitly set tonull.From local debugging, this appears to come from
pkg/engine/datasource/grpc_datasource/compiler.go, where nested message fields in the default branch are always built andmessage.Set(...)is always called without an existence check.Steps to Reproduce
filter: FilterInput.{ "query": "mutation UpdateRule($input: UpdateRuleInput!) { updateRule(input: $input) { id } }", "variables": { "input": { "id": "r1" } } }"filter": null.Expected Result
null, the nested message should also be omitted, or at least preserve null semantics consistently.{}.Actual Result
Both of these inputs:
{"input":{"id":"r1"}}and
{"input":{"id":"r1","filter":null}}produce a protobuf payload equivalent to:
{"input":{"id":"r1","filter":{}}}That makes downstream services unable to distinguish omitted vs null vs empty-object input.
Environment information
Environment
OS: macOS (Apple Silicon)
Package Manager: Go modules
Compiler(if manually compiled): go1.23.0
Router configuration
Router execution config
{ "kind": "GRAPHQL", "customGraphql": { "grpc": { "mapping": { "service": "RpcService" }, "protoSchema": "message UpdateRuleInput { string id = 1; FilterInput filter = 2; } message FilterInput { google.protobuf.StringValue key = 1; google.protobuf.StringValue operator = 2; }" } } }Log output
No router panic is required to reproduce this. The issue is visible in the compiled protobuf request: omitted/null nested input object becomes an empty message.Additional context
Optional scalar wrappers already seem to do an existence check before serializing. The behavior difference only appears on ordinary nested message fields.