Skip to content

Commit 37fa39d

Browse files
authored
[8.x] Added query param ?include_source_on_error for ingest requests (elastic#120725) (elastic#121010)
A new query parameter `?include_source_on_error` was added for create / index, update and bulk REST APIs to control if to include the document source in the error response in case of parsing errors. The default value is `true`. Relates to ES-9186.
1 parent 6e3592e commit 37fa39d

File tree

30 files changed

+418
-66
lines changed

30 files changed

+418
-66
lines changed

docs/changelog/120725.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
pr: 120725
2+
summary: |-
3+
A new query parameter `?include_source_on_error` was added for create / index, update and bulk REST APIs to control
4+
if to include the document source in the error response in case of parsing errors. The default value is `true`.
5+
area: Infra/REST API
6+
type: enhancement
7+
issues: []

libs/x-content/impl/src/main/java/org/elasticsearch/xcontent/provider/XContentParserConfigurationImpl.java

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ public class XContentParserConfigurationImpl implements XContentParserConfigurat
3131
RestApiVersion.current(),
3232
null,
3333
null,
34-
false
34+
false,
35+
true
3536
);
3637

3738
final NamedXContentRegistry registry;
@@ -40,21 +41,45 @@ public class XContentParserConfigurationImpl implements XContentParserConfigurat
4041
final FilterPath[] includes;
4142
final FilterPath[] excludes;
4243
final boolean filtersMatchFieldNamesWithDots;
44+
final boolean includeSourceOnError;
4345

4446
private XContentParserConfigurationImpl(
4547
NamedXContentRegistry registry,
4648
DeprecationHandler deprecationHandler,
4749
RestApiVersion restApiVersion,
4850
FilterPath[] includes,
4951
FilterPath[] excludes,
50-
boolean filtersMatchFieldNamesWithDots
52+
boolean filtersMatchFieldNamesWithDots,
53+
boolean includeSourceOnError
5154
) {
5255
this.registry = registry;
5356
this.deprecationHandler = deprecationHandler;
5457
this.restApiVersion = restApiVersion;
5558
this.includes = includes;
5659
this.excludes = excludes;
5760
this.filtersMatchFieldNamesWithDots = filtersMatchFieldNamesWithDots;
61+
this.includeSourceOnError = includeSourceOnError;
62+
}
63+
64+
@Override
65+
public boolean includeSourceOnError() {
66+
return includeSourceOnError;
67+
}
68+
69+
@Override
70+
public XContentParserConfiguration withIncludeSourceOnError(boolean includeSourceOnError) {
71+
if (includeSourceOnError == this.includeSourceOnError) {
72+
return this;
73+
}
74+
return new XContentParserConfigurationImpl(
75+
registry,
76+
deprecationHandler,
77+
restApiVersion,
78+
includes,
79+
excludes,
80+
filtersMatchFieldNamesWithDots,
81+
includeSourceOnError
82+
);
5883
}
5984

6085
@Override
@@ -65,7 +90,8 @@ public XContentParserConfigurationImpl withRegistry(NamedXContentRegistry regist
6590
restApiVersion,
6691
includes,
6792
excludes,
68-
filtersMatchFieldNamesWithDots
93+
filtersMatchFieldNamesWithDots,
94+
includeSourceOnError
6995
);
7096
}
7197

@@ -80,7 +106,8 @@ public XContentParserConfiguration withDeprecationHandler(DeprecationHandler dep
80106
restApiVersion,
81107
includes,
82108
excludes,
83-
filtersMatchFieldNamesWithDots
109+
filtersMatchFieldNamesWithDots,
110+
includeSourceOnError
84111
);
85112
}
86113

@@ -95,7 +122,8 @@ public XContentParserConfiguration withRestApiVersion(RestApiVersion restApiVers
95122
restApiVersion,
96123
includes,
97124
excludes,
98-
filtersMatchFieldNamesWithDots
125+
filtersMatchFieldNamesWithDots,
126+
includeSourceOnError
99127
);
100128
}
101129

@@ -143,7 +171,8 @@ public XContentParserConfiguration withFiltering(
143171
restApiVersion,
144172
includePaths,
145173
excludePaths,
146-
filtersMatchFieldNamesWithDots
174+
filtersMatchFieldNamesWithDots,
175+
includeSourceOnError
147176
);
148177
}
149178

libs/x-content/impl/src/main/java/org/elasticsearch/xcontent/provider/json/JsonXContentImpl.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,23 +87,30 @@ public XContentGenerator createGenerator(OutputStream os, Set<String> includes,
8787
return new JsonXContentGenerator(jsonFactory.createGenerator(os, JsonEncoding.UTF8), os, includes, excludes);
8888
}
8989

90+
private XContentParser createParser(XContentParserConfiguration config, JsonParser parser) {
91+
if (config.includeSourceOnError() == false) {
92+
parser.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); // enabled by default, disable if requested
93+
}
94+
return new JsonXContentParser(config, parser);
95+
}
96+
9097
@Override
9198
public XContentParser createParser(XContentParserConfiguration config, String content) throws IOException {
92-
return new JsonXContentParser(config, jsonFactory.createParser(content));
99+
return createParser(config, jsonFactory.createParser(content));
93100
}
94101

95102
@Override
96103
public XContentParser createParser(XContentParserConfiguration config, InputStream is) throws IOException {
97-
return new JsonXContentParser(config, jsonFactory.createParser(is));
104+
return createParser(config, jsonFactory.createParser(is));
98105
}
99106

100107
@Override
101108
public XContentParser createParser(XContentParserConfiguration config, byte[] data, int offset, int length) throws IOException {
102-
return new JsonXContentParser(config, jsonFactory.createParser(data, offset, length));
109+
return createParser(config, jsonFactory.createParser(data, offset, length));
103110
}
104111

105112
@Override
106113
public XContentParser createParser(XContentParserConfiguration config, Reader reader) throws IOException {
107-
return new JsonXContentParser(config, jsonFactory.createParser(reader));
114+
return createParser(config, jsonFactory.createParser(reader));
108115
}
109116
}

libs/x-content/src/main/java/org/elasticsearch/xcontent/XContentParserConfiguration.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ public interface XContentParserConfiguration {
2626
*/
2727
XContentParserConfiguration EMPTY = XContentProvider.provider().empty();
2828

29+
/**
30+
* Disable to not include the source in case of parsing errors (defaults to true).
31+
*/
32+
XContentParserConfiguration withIncludeSourceOnError(boolean includeSourceOnError);
33+
34+
boolean includeSourceOnError();
35+
2936
/**
3037
* Replace the registry backing {@link XContentParser#namedObject}.
3138
*/

libs/x-content/src/test/java/org/elasticsearch/xcontent/XContentParserTests.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.elasticsearch.xcontent.json.JsonXContent;
1717

1818
import java.io.IOException;
19+
import java.nio.charset.StandardCharsets;
1920
import java.util.Arrays;
2021
import java.util.Collections;
2122
import java.util.HashMap;
@@ -33,6 +34,7 @@
3334
import static org.hamcrest.Matchers.in;
3435
import static org.hamcrest.Matchers.instanceOf;
3536
import static org.hamcrest.Matchers.is;
37+
import static org.hamcrest.Matchers.not;
3638
import static org.hamcrest.Matchers.nullValue;
3739
import static org.junit.internal.matchers.ThrowableMessageMatcher.hasMessage;
3840

@@ -655,6 +657,25 @@ public void testCreateRootSubParser() throws IOException {
655657

656658
}
657659

660+
public void testJsonIncludeSourceOnParserError() throws IOException {
661+
var xContent = XContentFactory.xContent(XContentType.JSON);
662+
var source = "{\"field\": invalid}"; // causes parse exception
663+
var sourceEnabled = XContentParserConfiguration.EMPTY;
664+
var sourceDisabled = XContentParserConfiguration.EMPTY.withIncludeSourceOnError(false);
665+
666+
var parseException = expectThrows(XContentParseException.class, () -> createParser(xContent, sourceEnabled, source).map());
667+
assertThat(parseException.getMessage(), containsString(source));
668+
669+
parseException = expectThrows(XContentParseException.class, () -> createParser(xContent, sourceDisabled, source).map());
670+
assertThat(parseException.getMessage(), not(containsString(source)));
671+
}
672+
673+
private XContentParser createParser(XContent xContent, XContentParserConfiguration config, String content) throws IOException {
674+
return randomBoolean()
675+
? xContent.createParser(config, content)
676+
: xContent.createParser(config, content.getBytes(StandardCharsets.UTF_8));
677+
}
678+
658679
/**
659680
* Generates a random object {"first_field": "foo", "marked_field": {...random...}, "last_field": "bar}
660681
*

rest-api-spec/src/main/resources/rest-api-spec/api/bulk.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@
8787
"list_executed_pipelines": {
8888
"type": "boolean",
8989
"description": "Sets list_executed_pipelines for all incoming documents. Defaults to unset (false)"
90+
},
91+
"include_source_on_error": {
92+
"type": "boolean",
93+
"description": "True or false if to include the document source in the error message in case of parsing errors. Defaults to true."
9094
}
9195
},
9296
"body":{

rest-api-spec/src/main/resources/rest-api-spec/api/create.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@
6969
"pipeline":{
7070
"type":"string",
7171
"description":"The pipeline id to preprocess incoming documents with"
72+
},
73+
"include_source_on_error": {
74+
"type": "boolean",
75+
"description": "True or false if to include the document source in the error message in case of parsing errors. Defaults to true."
7276
}
7377
},
7478
"body":{

rest-api-spec/src/main/resources/rest-api-spec/api/index.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@
105105
"require_data_stream": {
106106
"type": "boolean",
107107
"description": "When true, requires the destination to be a data stream (existing or to-be-created). Default is false"
108+
},
109+
"include_source_on_error": {
110+
"type": "boolean",
111+
"description": "True or false if to include the document source in the error message in case of parsing errors. Defaults to true."
108112
}
109113
},
110114
"body":{

rest-api-spec/src/main/resources/rest-api-spec/api/update.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@
8383
"require_alias": {
8484
"type": "boolean",
8585
"description": "When true, requires destination is an alias. Default is false"
86+
},
87+
"include_source_on_error": {
88+
"type": "boolean",
89+
"description": "True or false if to include the document source in the error message in case of parsing errors. Defaults to true."
8690
}
8791
},
8892
"body":{
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.rest.action.document;
11+
12+
import org.elasticsearch.client.Request;
13+
import org.elasticsearch.client.Response;
14+
import org.elasticsearch.client.ResponseException;
15+
import org.elasticsearch.common.Strings;
16+
import org.elasticsearch.common.io.Streams;
17+
import org.elasticsearch.rest.RestUtils;
18+
import org.elasticsearch.test.ESIntegTestCase;
19+
20+
import java.io.InputStreamReader;
21+
22+
import static java.nio.charset.StandardCharsets.UTF_8;
23+
import static org.hamcrest.Matchers.both;
24+
import static org.hamcrest.Matchers.containsString;
25+
import static org.hamcrest.Matchers.not;
26+
27+
public class RestBulkActionIT extends ESIntegTestCase {
28+
@Override
29+
protected boolean addMockHttpTransport() {
30+
return false;
31+
}
32+
33+
public void testBulkIndexWithSourceOnErrorDisabled() throws Exception {
34+
var source = "{\"field\": \"index\",}";
35+
var sourceEscaped = "{\\\"field\\\": \\\"index\\\",}";
36+
37+
var request = new Request("PUT", "/test_index/_bulk");
38+
request.setJsonEntity(Strings.format("{\"index\":{\"_id\":\"1\"}}\n%s\n", source));
39+
40+
Response response = getRestClient().performRequest(request);
41+
String responseContent = Streams.copyToString(new InputStreamReader(response.getEntity().getContent(), UTF_8));
42+
assertThat(responseContent, containsString(sourceEscaped));
43+
44+
request.addParameter(RestUtils.INCLUDE_SOURCE_ON_ERROR_PARAMETER, "false");
45+
46+
response = getRestClient().performRequest(request);
47+
responseContent = Streams.copyToString(new InputStreamReader(response.getEntity().getContent(), UTF_8));
48+
assertThat(
49+
responseContent,
50+
both(not(containsString(sourceEscaped))).and(
51+
containsString("REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled)")
52+
)
53+
);
54+
}
55+
56+
public void testBulkUpdateWithSourceOnErrorDisabled() throws Exception {
57+
var source = "{\"field\": \"index\",}";
58+
var sourceEscaped = "{\\\"field\\\": \\\"index\\\",}";
59+
60+
var request = new Request("PUT", "/test_index/_bulk");
61+
request.addParameter(RestUtils.INCLUDE_SOURCE_ON_ERROR_PARAMETER, "false");
62+
request.setJsonEntity(Strings.format("{\"update\":{\"_id\":\"1\"}}\n{\"doc\":%s}}\n", source));
63+
64+
// note: this behavior is not consistent with bulk index actions
65+
// In case of updates by doc, the source is eagerly parsed and will fail the entire request if it cannot be parsed
66+
var exception = assertThrows(ResponseException.class, () -> getRestClient().performRequest(request));
67+
String response = Streams.copyToString(new InputStreamReader(exception.getResponse().getEntity().getContent(), UTF_8));
68+
69+
assertThat(
70+
response,
71+
both(not(containsString(sourceEscaped))).and(
72+
containsString("REDACTED (`StreamReadFeature.INCLUDE_SOURCE_IN_LOCATION` disabled)")
73+
)
74+
);
75+
}
76+
}

0 commit comments

Comments
 (0)