{"record":{"id":"af9f836ea2285370","repo":"elastic/elasticsearch","slug":"unknown-key-for-a-value-string-in-project-routing","errorCode":null,"errorMessage":"Unknown key for a VALUE_STRING in [project_routing]","messagePattern":"Unknown key for a VALUE_STRING in \\[project_routing\\]","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java","lineNumber":115,"sourceCode":"            (searchRequest, bytes) -> {\n                SearchTemplateRequest searchTemplateRequest = SearchTemplateRequest.fromXContent(bytes);\n                /*\n                 * For multisearch requests, project_routing could appear within the request body as:\n                 * {\"project_routing\": ...}\n                 * {\"id\": ...}\n                 *\n                 * In such cases, it is picked up by MultiSearchRequest#readMultiLineFormat() and is associated with the\n                 * SearchRequest object that represents the corresponding msearch request. However, it could also erroneously\n                 * appear as:\n                 * {...}\n                 * {\"project_routing\": ..., \"id\": ...}\n                 *\n                 * This is because, the same parser is shared between _msearch/template and _search/template and the above\n                 * format is valid only for the latter. For this reason, we need to explicitly check if project_routing got\n                 * associated with the SearchTemplateRequest instead of SearchRequest and error out if needed.\n                 */\n                if (searchTemplateRequest.getProjectRouting() != null) {\n                    throw new IllegalArgumentException(\"Unknown key for a VALUE_STRING in [project_routing]\");\n                }\n                if (searchTemplateRequest.getScript() != null) {\n                    searchTemplateRequest.setRequest(searchRequest);\n                    multiRequest.add(searchTemplateRequest);\n                } else {\n                    throw new IllegalArgumentException(\"Malformed search template\");\n                }\n                RestSearchAction.validateSearchRequest(restRequest, searchRequest);\n            },\n            (k, v, r) -> false,\n            Optional.of(crossProjectEnabled),\n            multiRequest.getProjectRouting()\n        );\n        return multiRequest;\n    }\n\n    @Override\n    public boolean mediaTypesValid(RestRequest request) {","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java#L97-L133","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before — project_routing in the body of _msearch/template:\nPOST /_msearch/template\n{}\n{\"project_routing\": \"my-project\", \"id\": \"my-template\", \"params\": {}}\n\n// after — project_routing in the URL:\nPOST /_msearch/template?project_routing=my-project\n{}\n{\"id\": \"my-template\", \"params\": {}}","handlingStrategy":"validation","validationCode":"// Validate _msearch/template body does not contain project_routing in per-request lines\n// project_routing should only be in the URL query parameter\nString url = \"/_msearch/template\";\nif (projectRouting != null) {\n    url += \"?project_routing=\" + URLEncoder.encode(projectRouting, StandardCharsets.UTF_8);\n}\n// body lines should NOT include project_routing:\n// header: {\"index\": \"my-index\"}\n// body:   {\"id\": \"my-template\", \"params\": {...}}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["elasticsearch","lang-mustache","msearch-template","project-routing","rest-api"],"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-12T12:17:08.281Z"}