{"record":{"id":"1825cdb51250e443","repo":"elastic/elasticsearch","slug":"malformed-search-template","errorCode":null,"errorMessage":"Malformed search template","messagePattern":"Malformed search template","errorType":"validation","errorClass":"IllegalArgumentException","httpStatus":400,"severity":"error","filePath":"modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java","lineNumber":121,"sourceCode":"                 *\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) {\n        return super.mediaTypesValid(request) && XContentType.supportsDelimitedBulkRequests(request.getXContentType());\n    }\n\n    @Override\n    protected Set<String> responseParams() {\n        return RESPONSE_PARAMS;","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/elastic/elasticsearch/blob/db6a809a667c081ca1dc7500389d26975573215f/modules/lang-mustache/src/main/java/org/elasticsearch/script/mustache/RestMultiSearchTemplateAction.java#L103-L139","documentation":"Thrown by the _msearch/template REST handler when a per-line sub-request was parsed but its SearchTemplateRequest carries no script (neither inline 'source' nor stored 'id'). The parser populated the request object yet left getScript() null, meaning the body section did not contain a usable template reference. Elasticsearch treats this as a malformed request because every template sub-request must resolve to exactly one template.","triggerScenarios":"POSTing to _msearch/template with a request line/index header followed by a body line that omits both the inline 'source' object and the stored 'id' string (e.g. a body line that is just `{\"params\":{}}` or `{}`). Also triggered by a body line containing only params/explain/profile keys with no template identifier.","commonSituations":"Copy-pasting a _search/template body into _msearch/template and forgetting that each pair needs the template key. Truncating a multi-line NDJSON payload. Misnesting the 'id'/'source' field inside 'params' by mistake. Sending a header line where the body line was expected (shifting all pairs by one).","solutions":["Inspect the NDJSON: every even-indexed (0-based) line is a header, every odd-indexed line must be a JSON object containing either an inline 'source' template or a stored 'id', plus optional 'params'.","Add the missing template field: {\"id\":\"my_template\",\"params\":{...}} or {\"source\":{\"query\":{\"match_all\":{}}},\"params\":{}}.","Validate the request body with a single _search/template call first to confirm the template resolves, then wrap it in the _msearch/template line-pair format.","Check for stray blank lines or trailing newlines in the NDJSON body that shift header/body alignment."],"exampleFix":"// before (broken: body line has no template)\n{ \"index\": \"movies\" }\n{ \"params\": { \"q\": \"star\" } }\n// after\n{ \"index\": \"movies\" }\n{ \"id\": \"movie_search\", \"params\": { \"q\": \"star\" } }","handlingStrategy":"validation","validationCode":"// Before sending _msearch/template, verify each body line has a template id or source\nfunction validateMsearchTemplate(ndjson) {\n  const lines = ndjson.trim().split(/\\n/);\n  if (lines.length % 2 !== 0) throw new Error('Expected even number of NDJSON lines (header/body pairs)');\n  for (let i = 1; i < lines.length; i += 2) {\n    const body = JSON.parse(lines[i]);\n    if (!body.id && !body.source) {\n      throw new Error(`Body line ${i} missing 'id' or 'source' (would cause 'Malformed search template')`);\n    }\n  }\n  return true;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pair header and body lines; never send an odd count.","Unit-test request construction against a strict schema requiring id|source on each body line.","Log the exact NDJSON payload on failure to spot alignment issues fast."],"tags":["rest-api","search-template","mustache","msearch","request-validation"],"backgroundTag":null,"analyzedSha":"db6a809a667c081ca1dc7500389d26975573215f","analyzedAt":"2026-08-12T01:39:14.192Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}