elastic/elasticsearch · error · InvalidUserDataException
Invalid block quote starting at ${start} in: ${body}
Error message
Invalid block quote starting at ${start} in:
${body} What it means
replaceBlockQuote converts Kibana-style triple-quote ('"""') delimited strings into standard escaped JSON strings. It finds the opening '"""' and then searches for the closing '"""'; if the closing delimiter is never found (indexOf returns -1) the string is malformed and cannot be safely converted, so it throws with the byte offset of the unmatched opening quote.
Source
Thrown at build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/doc/RestTestsFromDocSnippetTask.java:139
* contain newlines and {@code "} without the normal JSON escaping.
* This has to add it.
*/
@PackageScope
static String replaceBlockQuote(String body) {
int start = body.indexOf("\"\"\"");
if (start < 0) {
return body;
}
/*
* 1.3 is a fairly wild guess of the extra space needed to hold
* the escaped string.
*/
StringBuilder result = new StringBuilder((int) (body.length() * 1.3));
int startOfNormal = 0;
while (start >= 0) {
int end = body.indexOf("\"\"\"", start + 3);
if (end < 0) {
throw new InvalidUserDataException("Invalid block quote starting at " + start + " in:\n" + body);
}
result.append(body.substring(startOfNormal, start));
result.append('"');
result.append(body.substring(start + 3, end).replace("\"", "\\\"").replace("\n", "\\n"));
result.append('"');
startOfNormal = end + 3;
start = body.indexOf("\"\"\"", startOfNormal);
}
result.append(body.substring(startOfNormal));
return result.toString();
}
private class TestBuilder {
/**
* These languages aren't supported by the syntax highlighter so we
* shouldn't use them.
*/
private static final List BAD_LANGUAGES = List.of("json", "javascript");View on GitHub (pinned to db6a809a66)
Solutions
- Find the '"""' at the reported 'start' offset and add the matching closing '"""' after the string content.
- If block-quoting is not needed, remove the '"""' entirely and use standard JSON string escaping (\" for quotes, \n for newlines).
- Validate that every '"""' in the snippet body is paired by counting occurrences (must be even).
Example fix
// before — unbalanced block quote
GET /_search
"""
{ "query": { "match_all": {} } }
// after — close the block quote
GET /_search
"""
{ "query": { "match_all": {} } }
""" Defensive patterns
Strategy: validation
Validate before calling
// Verify triple-quote delimiters are balanced before building
void checkBlockQuotes(String body, String snippetPath) {
int count = 0, idx = -1;
while ((idx = body.indexOf("\"\"\"", idx + 1)) != -1) count++;
if (count % 2 != 0) {
throw new IllegalStateException(
"Unbalanced '\"\"\"' block quote in " + snippetPath
+ ": found " + count + " delimiters (must be even)");
}
} Prevention
- Count '"""' occurrences in a snippet body — they must be even.
- Prefer standard JSON escaping (\", \n) over block quotes for short strings.
- When editing a block-quoted body, keep the closing '"""' visible while editing.
When it happens
Trigger: A CONSOLE snippet body or a console-result response contains a '"""' block-quote opener but no corresponding closing '"""', producing an unbalanced delimiter.
Common situations: Manually editing a JSON body in a doc snippet and deleting the closing '"""' by accident, or copy-pasting only part of a block-quoted string. The start offset in the message points at the orphaned opening delimiter.
Related errors
- Extra content ${message} ('${cutOutNoNl}') matching [${patte
- Didn't match ${pattern}: ${content}
- ${snippet}: No paired previous test
- ${snippet}: Result can't be first in file
- ${snippet}: Use `[source,console]` instead of `// CONSOLE`.
AI-assisted analysis of elastic/elasticsearch@db6a809a66 (2026-08-12).
Data as JSON: /api/errors/46b7f703086ea8f3.
Report an issue: GitHub.