elastic/elasticsearch · error · IllegalArgumentException
Unknown key for a VALUE_STRING in [project_routing]
Error message
Unknown key for a VALUE_STRING in [project_routing]
What it means
Thrown by RestMultiSearchTemplateAction.parseRequest() when project_routing appears in the per-request body (the second line of each request pair in _msearch/template NDJSON) instead of as a top-level URL query parameter. Because the parser is shared between _msearch/template and _search/template, project_routing in the body gets parsed into SearchTemplateRequest.getProjectRouting(). The handler explicitly checks this and rejects it, since for _msearch/template, project_routing must be a single URL-level parameter.
Source
Thrown at modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java:115
(searchRequest, bytes) -> {
SearchTemplateRequest searchTemplateRequest = SearchTemplateRequest.fromXContent(bytes);
/*
* For multisearch requests, project_routing could appear within the request body as:
* {"project_routing": ...}
* {"id": ...}
*
* In such cases, it is picked up by MultiSearchRequest#readMultiLineFormat() and is associated with the
* SearchRequest object that represents the corresponding msearch request. However, it could also erroneously
* appear as:
* {...}
* {"project_routing": ..., "id": ...}
*
* This is because, the same parser is shared between _msearch/template and _search/template and the above
* format is valid only for the latter. For this reason, we need to explicitly check if project_routing got
* associated with the SearchTemplateRequest instead of SearchRequest and error out if needed.
*/
if (searchTemplateRequest.getProjectRouting() != null) {
throw new IllegalArgumentException("Unknown key for a VALUE_STRING in [project_routing]");
}
if (searchTemplateRequest.getScript() != null) {
searchTemplateRequest.setRequest(searchRequest);
multiRequest.add(searchTemplateRequest);
} else {
throw new IllegalArgumentException("Malformed search template");
}
RestSearchAction.validateSearchRequest(restRequest, searchRequest);
},
(k, v, r) -> false,
Optional.of(crossProjectEnabled),
multiRequest.getProjectRouting()
);
return multiRequest;
}
@Override
public boolean mediaTypesValid(RestRequest request) {View on GitHub (pinned to db6a809a66)
Solutions
- Move project_routing to the URL query string: POST /_msearch/template?project_routing=<value>
- Remove project_routing from each per-request body line in the NDJSON payload
- Ensure the NDJSON body lines contain only template-specific fields (id, params, etc.), not routing parameters
- Note: this differs from _search/template where project_routing in the body IS valid
Example fix
// before — project_routing in the body of _msearch/template:
POST /_msearch/template
{}
{"project_routing": "my-project", "id": "my-template", "params": {}}
// after — project_routing in the URL:
POST /_msearch/template?project_routing=my-project
{}
{"id": "my-template", "params": {}} Defensive patterns
Strategy: validation
Validate before calling
// Validate _msearch/template body does not contain project_routing in per-request lines
// project_routing should only be in the URL query parameter
String url = "/_msearch/template";
if (projectRouting != null) {
url += "?project_routing=" + URLEncoder.encode(projectRouting, StandardCharsets.UTF_8);
}
// body lines should NOT include project_routing:
// header: {"index": "my-index"}
// body: {"id": "my-template", "params": {...}} Prevention
- For _msearch/template, always put project_routing in the URL query string, not the NDJSON body
- Remember that _search/template allows project_routing in the body, but _msearch/template does not
- Validate each body line contains only template fields (id, params, etc.)
- Document the difference in project_routing placement between _search/template and _msearch/template
When it happens
Trigger: Sending a _msearch/template request where one of the per-request body objects contains {"project_routing": "value", "id": "template_id"}. In _msearch/template, each request pair is (header_line, body_line). If project_routing is in the body_line, it gets parsed into the SearchTemplateRequest rather than the SearchRequest, and this check fires. The correct location is ?project_routing=value in the URL.
Common situations: Copying the _search/template body format (where project_routing in the body is valid) into a _msearch/template request. Confusing the per-request body format with the top-level parameter format. Cross-project Serverless mode where project_routing is relevant.
Related errors
- maxConcurrentSearchRequests must be positive
- project_routing already set
- invalid database configuration id [{}]: must not be null or
- invalid database configuration id [{}]: id doesn't match req
- Database {} is read only
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/af9f836ea2285370.
Report an issue: GitHub.