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

  1. Remove the recursive field from the query-path of the request message, or break the cycle with a wrapper message that stops recursion.
  2. Restructure so recursive/nested data is sent in the body (POST) rather than as query parameters.
  3. Use a depth-limited concrete type instead of a self-referential one for query fields.
  4. 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

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


AI-assisted analysis of grpc-ecosystem/grpc-gateway@a58a4436a3 (2026-09-02). Data as JSON: /api/errors/20d725d2bbdb457f. Report an issue: GitHub.