grpc-ecosystem/grpc-gateway · error
exceeded recursive count (%d) for query parameter %q
Error message
exceeded recursive count (%d) for query parameter %q
What it means
nestedQueryParams protects against infinitely recursive query parameters (a message containing itself, directly or transitively) using a cycle tracker. When cycle.Check(msg name) fails — the message already appeared in the current recursion path beyond the allowed count — generation stops with 'exceeded recursive count (%d) for query parameter %q'. OpenAPI query parameters cannot be rendered for cyclic structures.
Source
Thrown at protoc-gen-openapiv2/internal/genopenapi/template.go:454
}
}
valueComments := enumValueProtoComments(reg, enum)
if valueComments != "" {
param.Description = strings.TrimLeft(param.Description+"\n\n "+valueComments, "\n")
}
}
return []openapiParameterObject{param}, nil
}
// nested type, recurse
msg, err := reg.LookupMsg("", fieldType)
if err != nil {
return nil, fmt.Errorf("unknown message type %s", fieldType)
}
// Check for cyclical message reference:
if ok := cycle.Check(*msg.Name); !ok {
return nil, fmt.Errorf("exceeded recursive count (%d) for query parameter %q", cycle.count, fieldType)
}
// Construct a new map with the message name so a cycle further down the recursive path can be detected.
// Do not keep anything in the original touched reference and do not pass that reference along. This will
// prevent clobbering adjacent records while recursing.
touchedOut := cycle.Branch()
for _, nestedField := range msg.Fields {
if !isVisible(getFieldVisibilityOption(nestedField), reg) {
continue
}
fieldName := reg.FieldName(field)
p, err := nestedQueryParams(msg, nestedField, prefix+fieldName+".", reg, pathParams, body, touchedOut)
if err != nil {
return nil, err
}
params = append(params, p...)View on GitHub (pinned to a58a4436a3)
Solutions
- Remove the recursive field from the query-path of the request message, or break the cycle with a wrapper message that stops recursion.
- Restructure so recursive/nested data is sent in the body (POST) rather than as query parameters.
- Use a depth-limited concrete type instead of a self-referential one for query fields.
- If the recursion is intentional but deep, flatten the fields the client should pass as query params.
Example fix
// before
message Node { string id = 1; Node child = 2; }
rpc Get(GetRequest) returns ...; // GetRequest uses Node in query
// after
message Node { string id = 1; } // move recursive child out of query type or into body Defensive patterns
Strategy: validation
Validate before calling
// Static check: refuse self-referential messages in GET query requests
func hasCycle(m *Message, seen map[string]bool) bool {
if seen[m.Name] { return true }
seen[m.Name] = true
for _, f := range m.Fields {
if f.IsMessage && hasCycle(f.MessageType, seen) { return true }
}
return false
} Prevention
- Never use recursive message types as GET query request fields.
- Send recursive/tree data via request bodies (POST) instead.
- Add proto lint rules (buf) rejecting self-referential query types.
When it happens
Trigger: A GET request proto has a nested message query field that (transitively) references itself — e.g. message A { A child; } or A -> B -> A — and the recursion depth exceeds the cycle count limit during nestedQueryParams recursion.
Common situations: Tree-like protos (parent/child, node graphs) used as GET query request types; reusing a generic recursive envelope message in query requests; accidental self-reference added during refactoring.
Related errors
- failed to encode OpenAPI for %s: %w
- unknown enum type %s
- unknown message type %s
- unsupported type: %q
- can't resolve OpenAPI name from %q
AI-assisted analysis of grpc-ecosystem/grpc-gateway@a58a4436a3 (2026-09-02).
Data as JSON: /api/errors/20d725d2bbdb457f.
Report an issue: GitHub.