Skip to content

router: grpc datasource serializes omitted or null nested input objects as empty protobuf messages #2650

@fengyuwusong

Description

@fengyuwusong

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

  1. Start the router with a gRPC datasource whose input contains an optional nested message field like filter: FilterInput.
  2. Send this request with the field omitted:
{
  "query": "mutation UpdateRule($input: UpdateRuleInput!) { updateRule(input: $input) { id } }",
  "variables": {
    "input": {
      "id": "r1"
    }
  }
}
  1. Inspect the compiled/proxied protobuf request.
  2. 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:

{"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

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.

Metadata

Metadata

Assignees

Labels

internally-reviewedThe issue has been reviewed internally.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions