Skip to content

grpc-gateway: repeated field as a single query parameter fails to transcode (400 "failed to encode payload") #14907

Description

@CallMeCCLemon

Is there an existing issue for this?

  • I have searched the existing issues

Kong version ($ kong version)

Reproduced through the kong/ingress Helm chart (Kong Ingress Controller). The root-cause code path is unchanged on master (7772bd8b3125a57fe457b9e5947faa3f3bdf59bc), so current 3.x is affected (latest release at filing: 3.9.2).

Current Behavior

The grpc-gateway plugin returns HTTP 400 {"message":"failed to encode payload"} when a repeated request field is supplied as a single query-parameter occurrence.

For a method whose request message has e.g. repeated Color colors = N bound to a GET via google.api.http:

Request Result
?colors=RED  (one occurrence) 400 failed to encode payload
?colors=RED&colors=BLUE  (two occurrences) 200 ✅ (reaches upstream)
scalar field, e.g. ?name=bolt 200
single (non-repeated) enum, e.g. ?mode=AT_MOST 200

So a repeated field is only transcodable when the client happens to pass two or more values; a single value fails. The single-value case is the common one (one selected filter), and it is deterministic — 20/20 requests returned 400 in our deployment.

Expected Behavior

A repeated field with a single query value should transcode to a one-element list (?colors=REDcolors = [RED]), matching grpc-ecosystem/grpc-gateway query-parameter semantics.

Steps To Reproduce

repro.proto:

syntax = "proto3";
package repro;
import "google/api/annotations.proto";

enum Color { COLOR_UNSPECIFIED = 0; RED = 1; BLUE = 2; }

message GetReq {
  Color          one  = 1;   // single enum   -> OK
  repeated Color many = 2;   // repeated enum -> 400 on a single value
}
message GetResp { string ok = 1; }

service Repro {
  rpc Get(GetReq) returns (GetResp) {
    option (google.api.http) = { get: "/get" };
  }
}

kong.yml (DB-less):

_format_version: "3.0"
services:
  - name: repro
    protocol: grpc
    host: 127.0.0.1
    port: 9999          # no real upstream needed: the 400 is raised during
    routes:             # request transcoding, before the upstream is contacted
      - name: repro
        protocols: [http]
        paths: ["/"]
    plugins:
      - name: grpc-gateway
        config:
          proto: /repro.proto

Run Kong and probe:

docker run --rm -p 8000:8000 \
  -e KONG_DATABASE=off \
  -e KONG_DECLARATIVE_CONFIG=/kong.yml \
  -e KONG_PROXY_LISTEN="0.0.0.0:8000" \
  -v "$PWD/kong.yml:/kong.yml" \
  -v "$PWD/repro.proto:/repro.proto" \
  kong:3.9

curl -s -o /dev/null -w "%{http_code}\n" 'localhost:8000/get?many=RED'           # 400  <-- bug
curl -s -o /dev/null -w "%{http_code}\n" 'localhost:8000/get?many=RED&many=BLUE' # not 400 (transcodes)
curl -s -o /dev/null -w "%{http_code}\n" 'localhost:8000/get?one=RED'            # not 400 (scalar enum)
curl -s 'localhost:8000/get?many=RED'                                            # {"message":"failed to encode payload"}

(repro.proto imports google/api/annotations.proto; Kong bundles the google protos on the plugin include path. If your build doesn't, mount google/api/annotations.proto + google/api/http.proto next to repro.proto.)

Anything else?

Root causekong/plugins/grpc-gateway/deco.lua. add_to_table assigns the query value to the leaf field with no handling for repeated fields:

local function add_to_table( t, path, v, typ )
local tab = t -- set up pointer to table root
local msg_typ = typ;
for m in re_gmatch( path , "([^.]+)(\\.)?", "jo" ) do
local key, dot = m[1], m[2]
msg_typ = get_field_type(msg_typ, key)
-- not argument that we concern with
if not msg_typ then
return
end
if dot then
tab[key] = tab[key] or {} -- create empty nested table if key does not exist
tab = tab[key]
else
tab[key] = encode_fix(v, msg_typ)
end
end

local function add_to_table( t, path, v, typ )
  ...
    else
      tab[key] = encode_fix(v, msg_typ)   -- assigns scalar; a repeated field needs an array
    end
  ...
end

and encode_fix only special-cases bool:

local function encode_fix(v, typ)
if typ == "bool" then
-- special case for URI parameters
return v and v ~= "0" and v ~= "false"
end
return v
end
--[[
// Set value `v` at `path` in table `t`
// Path contains value address in dot-syntax. For example:
// `path="a.b.c"` would lead to `t[a][b][c] = v`.

ngx.req.get_uri_args() returns a string for a single-occurrence key and a table for multiple occurrences. For a repeated field, the single-string case is assigned directly, then pb.encode rejects it (it expects an array) and the plugin returns failed to encode payload:

local pok, msg = pcall(pb.encode, self.endpoint.input_type, payload)
if not pok or not msg then
if msg then
ngx.log(ngx.ERR, msg)
end
-- should return error msg to client?
return nil, "failed to encode payload"

With two occurrences the value is already a table, so it encodes — which is why single-vs-multiple behave differently.

Suggested fix: in add_to_table, when the resolved leaf field is repeated (descriptor label LABEL_REPEATED), normalize a non-table v to { v } (mapping each element through encode_fix) before assignment, so a single query value becomes a one-element list. This matches upstream grpc-gateway behavior.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions