diff --git a/CHANGELOG.md b/CHANGELOG.md index b62caa54d7d8a..c32ae4aa7b7f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Add async periodic flush task support for pull-based ingestion ([#19878](https://github.com/opensearch-project/OpenSearch/pull/19878)) - Add support for context aware segments ([#19098](https://github.com/opensearch-project/OpenSearch/pull/19098)) - Implement GRPC FunctionScoreQuery ([#19888](https://github.com/opensearch-project/OpenSearch/pull/19888)) +- Implement error_trace parameter for bulk requests ([#19985](https://github.com/opensearch-project/OpenSearch/pull/19985)) - Allow the truncate filter in normalizers ([#19778](https://github.com/opensearch-project/OpenSearch/issues/19778)) - Support pull-based ingestion message mappers and raw payload support ([#19765](https://github.com/opensearch-project/OpenSearch/pull/19765)] diff --git a/qa/smoke-test-http/src/test/java/org/opensearch/http/DetailedErrorsDisabledIT.java b/qa/smoke-test-http/src/test/java/org/opensearch/http/DetailedErrorsDisabledIT.java index 0c845bb2d34e5..cb8951c860a39 100644 --- a/qa/smoke-test-http/src/test/java/org/opensearch/http/DetailedErrorsDisabledIT.java +++ b/qa/smoke-test-http/src/test/java/org/opensearch/http/DetailedErrorsDisabledIT.java @@ -34,17 +34,26 @@ import java.io.IOException; +import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.opensearch.action.search.MultiSearchRequest; +import org.opensearch.action.search.SearchRequest; import org.opensearch.client.Request; import org.opensearch.client.Response; import org.opensearch.client.ResponseException; import org.opensearch.common.settings.Settings; +import org.opensearch.core.xcontent.MediaType; +import org.opensearch.core.xcontent.MediaTypeRegistry; +import org.opensearch.index.query.QueryStringQueryBuilder; +import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.test.OpenSearchIntegTestCase.ClusterScope; import org.opensearch.test.OpenSearchIntegTestCase.Scope; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; /** * Tests that when disabling detailed errors, a request with the error_trace parameter returns an HTTP 400 response. @@ -73,4 +82,19 @@ public void testThatErrorTraceParamReturns400() throws IOException, ParseExcepti containsString("\"error\":\"error traces in responses are disabled.\"")); assertThat(response.getStatusLine().getStatusCode(), is(400)); } + + public void testDetailedStackTracesAreNotIncludedWhenErrorTraceIsDisabledForBulkApis() throws IOException, ParseException { + MediaType contentType = MediaTypeRegistry.JSON; + MultiSearchRequest multiSearchRequest = new MultiSearchRequest().add( + new SearchRequest("missing_index") + .source(new SearchSourceBuilder().query(new QueryStringQueryBuilder("foo").field("*")))); + Request request = new Request("POST", "/_msearch"); + byte[] requestBody = MultiSearchRequest.writeMultiLineFormat(multiSearchRequest, contentType.xContent()); + request.setEntity(new ByteArrayEntity(requestBody, ContentType.APPLICATION_JSON)); + + Response response = getRestClient().performRequest(request); + + assertThat(EntityUtils.toString(response.getEntity()), not(containsString("stack_trace"))); + } + } diff --git a/qa/smoke-test-http/src/test/java/org/opensearch/http/DetailedErrorsEnabledIT.java b/qa/smoke-test-http/src/test/java/org/opensearch/http/DetailedErrorsEnabledIT.java index 76f801c75d866..b105ddb097c94 100644 --- a/qa/smoke-test-http/src/test/java/org/opensearch/http/DetailedErrorsEnabledIT.java +++ b/qa/smoke-test-http/src/test/java/org/opensearch/http/DetailedErrorsEnabledIT.java @@ -32,16 +32,24 @@ package org.opensearch.http; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; + +import java.io.IOException; + +import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.opensearch.action.search.MultiSearchRequest; +import org.opensearch.action.search.SearchRequest; import org.opensearch.client.Request; import org.opensearch.client.Response; import org.opensearch.client.ResponseException; - -import java.io.IOException; - -import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.not; +import org.opensearch.core.xcontent.MediaType; +import org.opensearch.core.xcontent.MediaTypeRegistry; +import org.opensearch.index.query.QueryStringQueryBuilder; +import org.opensearch.search.builder.SearchSourceBuilder; /** * Tests that by default the error_trace parameter can be used to show stacktraces @@ -54,23 +62,40 @@ public void testThatErrorTraceWorksByDefault() throws IOException, ParseExceptio request.addParameter("error_trace", "true"); getRestClient().performRequest(request); fail("request should have failed"); - } catch(ResponseException e) { + } catch (ResponseException e) { Response response = e.getResponse(); assertThat(response.getHeader("Content-Type"), containsString("application/json")); assertThat(EntityUtils.toString(response.getEntity()), - containsString("\"stack_trace\":\"OpenSearchException[Validation Failed: 1: index / indices is missing;]; " + + containsString("\"stack_trace\":\"OpenSearchException[Validation Failed: 1: index / indices is missing;]; " + "nested: ActionRequestValidationException[Validation Failed: 1:")); } try { getRestClient().performRequest(new Request("DELETE", "/")); fail("request should have failed"); - } catch(ResponseException e) { + } catch (ResponseException e) { Response response = e.getResponse(); assertThat(response.getHeader("Content-Type"), containsString("application/json; charset=UTF-8")); assertThat(EntityUtils.toString(response.getEntity()), - not(containsString("\"stack_trace\":\"[Validation Failed: 1: index / indices is missing;]; " + not(containsString("\"stack_trace\":\"[Validation Failed: 1: index / indices is missing;]; " + "nested: ActionRequestValidationException[Validation Failed: 1:"))); } } + + public void testDetailedStackTracesAreIncludedWhenErrorTraceIsExplicitlyEnabledForBulkApis() throws IOException, ParseException { + MediaType contentType = MediaTypeRegistry.JSON; + MultiSearchRequest multiSearchRequest = new MultiSearchRequest().add( + new SearchRequest("missing_index") + .source(new SearchSourceBuilder().query(new QueryStringQueryBuilder("foo").field("*")))); + Request request = new Request("POST", "/_msearch"); + request.addParameter("error_trace", "true"); + byte[] requestBody = MultiSearchRequest.writeMultiLineFormat(multiSearchRequest, contentType.xContent()); + request.setEntity(new ByteArrayEntity(requestBody, ContentType.APPLICATION_JSON)); + + Response response = getRestClient().performRequest(request); + + assertThat(EntityUtils.toString(response.getEntity()), + containsString("\"stack_trace\":\"[missing_index] IndexNotFoundException[no such index [missing_index]]")); + } + } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/bulk/100_error_traces.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/bulk/100_error_traces.yml new file mode 100644 index 0000000000000..0908b1d957f3b --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/bulk/100_error_traces.yml @@ -0,0 +1,34 @@ +setup: + - skip: + version: " - 3.3.99" + reason: "Error trace support was added in 3.4" +--- +"Returns a detailed error stack trace when requested": + + - do: + bulk: + error_trace: true + body: + - '{"update": {"_index": "index_does_not_exist_321", "_id": "123"}}' + - '{"doc": {"field": "value"}}' + + - match: { errors: true } + - match: { items.0.update.status: 404 } + - match: { items.0.update.error.type: document_missing_exception } + - match: { items.0.update.error.reason: "[123]: document missing" } + - match: { items.0.update.error.stack_trace: "/.*\\[\\[index_does_not_exist_321\\]\\[0\\].*DocumentMissingException\\[\\[123\\]:.*document.*missing\\].*/" } +--- +"Skips a detailed error stack trace when requested": + + - do: + bulk: + error_trace: false + body: + - '{"update": {"_index": "index_does_not_exist_321", "_id": "123"}}' + - '{"doc": {"field": "value"}}' + + - match: { errors: true } + - match: { items.0.update.status: 404 } + - match: { items.0.update.error.type: document_missing_exception } + - match: { items.0.update.error.reason: "[123]: document missing" } + - match: { items.0.update.error.stack_trace: null } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/mget/90_error_traces.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/mget/90_error_traces.yml new file mode 100644 index 0000000000000..288ae9e1f05bf --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/mget/90_error_traces.yml @@ -0,0 +1,36 @@ +setup: + - skip: + version: " - 3.3.99" + reason: "Error trace support was added in 3.4" +--- +"Returns a detailed error stack trace when requested": + + - do: + mget: + error_trace: true + index: index_does_not_exist_321 + body: + ids: [ 1 ] + + - match: { docs.0.error.type: index_not_found_exception } + - match: { docs.0.error.reason: "no such index [index_does_not_exist_321]" } + - match: { docs.0.error.stack_trace: "/.*\\[index_does_not_exist_321\\].*IndexNotFoundException.*/" } + - match: { docs.0.error.root_cause.0.type: index_not_found_exception } + - match: { docs.0.error.root_cause.0.reason: "no such index [index_does_not_exist_321]" } + - match: { docs.0.error.root_cause.0.stack_trace: "/.*\\[index_does_not_exist_321\\].*IndexNotFoundException.*/" } +--- +"Skips a detailed error stack trace when requested": + + - do: + mget: + error_trace: false + index: index_does_not_exist_321 + body: + ids: [ 1 ] + + - match: { docs.0.error.type: index_not_found_exception } + - match: { docs.0.error.reason: "no such index [index_does_not_exist_321]" } + - match: { docs.0.error.stack_trace: null } + - match: { docs.0.error.root_cause.0.type: index_not_found_exception } + - match: { docs.0.error.root_cause.0.reason: "no such index [index_does_not_exist_321]" } + - match: { docs.0.error.root_cause.0.stack_trace: null } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/msearch/30_error_traces.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/msearch/30_error_traces.yml new file mode 100644 index 0000000000000..1b89355fbc732 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/msearch/30_error_traces.yml @@ -0,0 +1,40 @@ +setup: + - skip: + version: " - 3.3.99" + reason: "Error trace support was added in 3.4" +--- +"Returns a detailed error stack trace when requested": + + - do: + msearch: + error_trace: true + body: + - index: index_does_not_exist_321 + - query: + match: { foo: foo } + + - match: { responses.0.status: 404 } + - match: { responses.0.error.type: index_not_found_exception } + - match: { responses.0.error.reason: "no such index [index_does_not_exist_321]" } + - match: { responses.0.error.stack_trace: "/.*\\[index_does_not_exist_321\\].*IndexNotFoundException.*/" } + - match: { responses.0.error.root_cause.0.type: index_not_found_exception } + - match: { responses.0.error.root_cause.0.reason: "no such index [index_does_not_exist_321]" } + - match: { responses.0.error.root_cause.0.stack_trace: "/.*\\[index_does_not_exist_321\\].*IndexNotFoundException.*/" } +--- +"Skips a detailed error stack trace when requested": + + - do: + msearch: + error_trace: false + body: + - index: index_does_not_exist_321 + - query: + match: { foo: foo } + + - match: { responses.0.status: 404 } + - match: { responses.0.error.type: index_not_found_exception } + - match: { responses.0.error.reason: "no such index [index_does_not_exist_321]" } + - match: { responses.0.error.stack_trace: null } + - match: { responses.0.error.root_cause.0.type: index_not_found_exception } + - match: { responses.0.error.root_cause.0.reason: "no such index [index_does_not_exist_321]" } + - match: { responses.0.error.root_cause.0.stack_trace: null } diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/mtermvectors/30_error_traces.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/mtermvectors/30_error_traces.yml new file mode 100644 index 0000000000000..84b8874ace6e8 --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/mtermvectors/30_error_traces.yml @@ -0,0 +1,34 @@ +setup: + - skip: + version: " - 3.3.99" + reason: "Error trace support was added in 3.4" +--- +"Returns a detailed error stack trace when requested": + + - do: + mtermvectors: + "error_trace": true + "index": "index_does_not_exist_321" + "ids": [ "testing_document" ] + + - match: { docs.0.error.type: index_not_found_exception } + - match: { docs.0.error.reason: "no such index [index_does_not_exist_321]" } + - match: { docs.0.error.stack_trace: "/.*\\[index_does_not_exist_321\\].*IndexNotFoundException.*/" } + - match: { docs.0.error.root_cause.0.type: index_not_found_exception } + - match: { docs.0.error.root_cause.0.reason: "no such index [index_does_not_exist_321]" } + - match: { docs.0.error.root_cause.0.stack_trace: "/.*\\[index_does_not_exist_321\\].*IndexNotFoundException.*/" } +--- +"Skips a detailed error stack trace when requested": + + - do: + mtermvectors: + "error_trace": false + "index": "index_does_not_exist_321" + "ids": [ "testing_document" ] + + - match: { docs.0.error.type: index_not_found_exception } + - match: { docs.0.error.reason: "no such index [index_does_not_exist_321]" } + - match: { docs.0.error.stack_trace: null } + - match: { docs.0.error.root_cause.0.type: index_not_found_exception } + - match: { docs.0.error.root_cause.0.reason: "no such index [index_does_not_exist_321]" } + - match: { docs.0.error.root_cause.0.stack_trace: null } diff --git a/server/src/main/java/org/opensearch/rest/AbstractRestChannel.java b/server/src/main/java/org/opensearch/rest/AbstractRestChannel.java index 11a116e8c858d..6834abebbf704 100644 --- a/server/src/main/java/org/opensearch/rest/AbstractRestChannel.java +++ b/server/src/main/java/org/opensearch/rest/AbstractRestChannel.java @@ -64,6 +64,7 @@ public abstract class AbstractRestChannel implements RestChannel { private final boolean pretty; private final boolean human; private final String acceptHeader; + private final boolean detailedErrorStackTraceRequested; private BytesStreamOutput bytesOut; @@ -82,6 +83,7 @@ protected AbstractRestChannel(RestRequest request, boolean detailedErrorsEnabled this.filterPath = request.param("filter_path", null); this.pretty = request.paramAsBoolean("pretty", false); this.human = request.paramAsBoolean("human", false); + this.detailedErrorStackTraceRequested = request.paramAsBoolean("error_trace", false); } @Override @@ -189,4 +191,8 @@ public RestRequest request() { public boolean detailedErrorsEnabled() { return detailedErrorsEnabled; } + + public boolean detailedErrorStackTraceEnabled() { + return detailedErrorStackTraceRequested; + } } diff --git a/server/src/main/java/org/opensearch/rest/RestChannel.java b/server/src/main/java/org/opensearch/rest/RestChannel.java index b3ded1389f754..c2066202ee41d 100644 --- a/server/src/main/java/org/opensearch/rest/RestChannel.java +++ b/server/src/main/java/org/opensearch/rest/RestChannel.java @@ -66,5 +66,10 @@ XContentBuilder newBuilder(@Nullable MediaType mediaType, @Nullable MediaType re */ boolean detailedErrorsEnabled(); + /** + * @return true if detailed stack traces should be included in the response. + */ + boolean detailedErrorStackTraceEnabled(); + void sendResponse(RestResponse response); } diff --git a/server/src/main/java/org/opensearch/rest/RestController.java b/server/src/main/java/org/opensearch/rest/RestController.java index 57a0fd10772a1..5b0e45bdc2d27 100644 --- a/server/src/main/java/org/opensearch/rest/RestController.java +++ b/server/src/main/java/org/opensearch/rest/RestController.java @@ -436,7 +436,7 @@ private void tryAllHandlers(final RestRequest request, final RestChannel channel } // error_trace cannot be used when we disable detailed errors // we consume the error_trace parameter first to ensure that it is always consumed - if (request.paramAsBoolean("error_trace", false) && channel.detailedErrorsEnabled() == false) { + if (channel.detailedErrorStackTraceEnabled() && channel.detailedErrorsEnabled() == false) { channel.sendResponse( BytesRestResponse.createSimpleErrorResponse(channel, BAD_REQUEST, "error traces in responses are disabled.") ); @@ -636,6 +636,11 @@ public boolean detailedErrorsEnabled() { return delegate.detailedErrorsEnabled(); } + @Override + public boolean detailedErrorStackTraceEnabled() { + return delegate.detailedErrorStackTraceEnabled(); + } + @Override public void sendResponse(RestResponse response) { close(); @@ -699,6 +704,11 @@ public boolean detailedErrorsEnabled() { return delegate.detailedErrorsEnabled(); } + @Override + public boolean detailedErrorStackTraceEnabled() { + return delegate.detailedErrorStackTraceEnabled(); + } + @Override public void sendResponse(RestResponse response) { close(); diff --git a/server/src/main/java/org/opensearch/rest/action/RestStatusToXContentListener.java b/server/src/main/java/org/opensearch/rest/action/RestStatusToXContentListener.java index ae6795dd89b7b..4eb767ef2b950 100644 --- a/server/src/main/java/org/opensearch/rest/action/RestStatusToXContentListener.java +++ b/server/src/main/java/org/opensearch/rest/action/RestStatusToXContentListener.java @@ -69,7 +69,7 @@ public RestStatusToXContentListener(RestChannel channel, Function handlers = restController.getAllHandlers(); assertTrue(handlers.hasNext()); MethodHandlers faviconHandler = handlers.next(); - assertEquals(faviconHandler.getPath(), "/favicon.ico"); - assertEquals(faviconHandler.getValidMethods(), Set.of(RestRequest.Method.GET)); + assertEquals("/favicon.ico", faviconHandler.getPath()); + assertEquals(Set.of(RestRequest.Method.GET), faviconHandler.getValidMethods()); assertFalse(handlers.hasNext()); } @@ -154,13 +155,13 @@ public void testRestControllerGetAllHandlers() { assertTrue(handlers.hasNext()); MethodHandlers rootHandler = handlers.next(); - assertEquals(rootHandler.getPath(), "/foo"); - assertEquals(rootHandler.getValidMethods(), Set.of(RestRequest.Method.GET, RestRequest.Method.PATCH)); + assertEquals("/foo", rootHandler.getPath()); + assertEquals(Set.of(RestRequest.Method.GET, RestRequest.Method.PATCH), rootHandler.getValidMethods()); assertTrue(handlers.hasNext()); MethodHandlers faviconHandler = handlers.next(); - assertEquals(faviconHandler.getPath(), "/favicon.ico"); - assertEquals(faviconHandler.getValidMethods(), Set.of(RestRequest.Method.GET)); + assertEquals("/favicon.ico", faviconHandler.getPath()); + assertEquals(Set.of(RestRequest.Method.GET), faviconHandler.getValidMethods()); assertFalse(handlers.hasNext()); } @@ -595,6 +596,18 @@ public void testHandleBadRequestWithHtmlSpecialCharsInUri() { assertThat(channel.getRestResponse().content().utf8ToString(), containsString("invalid uri has been requested")); } + public void testHandleBadRequestWithInvalidCombinationForDetailedErrors() { + final FakeRestRequest fakeRestRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withPath("/foo") + .withMethod(RestRequest.Method.GET) + .withParams(Map.of("error_trace", "true")) + .build(); + final AssertingChannel channel = new AssertingChannel(fakeRestRequest, false, RestStatus.BAD_REQUEST); + restController.dispatchRequest(fakeRestRequest, channel, client.threadPool().getThreadContext()); + assertThat(channel.detailedErrorsEnabled(), equalTo(false)); + assertThat(channel.detailedErrorStackTraceEnabled(), equalTo(true)); + assertThat(channel.getRestResponse().content().utf8ToString(), containsString("error traces in responses are disabled.")); + } + public void testHandleBadInputWithCreateIndex() { final FakeRestRequest fakeRestRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withPath("/foo") .withMethod(RestRequest.Method.PUT) @@ -604,9 +617,60 @@ public void testHandleBadInputWithCreateIndex() { restController.registerHandler(RestRequest.Method.PUT, "/foo", new RestCreateIndexAction()); restController.dispatchRequest(fakeRestRequest, channel, client.threadPool().getThreadContext()); assertEquals( - channel.getRestResponse().content().utf8ToString(), - "{\"error\":{\"root_cause\":[{\"type\":\"not_x_content_exception\",\"reason\":\"Compressor detection can only be called on some xcontent bytes or compressed xcontent bytes\"}],\"type\":\"not_x_content_exception\",\"reason\":\"Compressor detection can only be called on some xcontent bytes or compressed xcontent bytes\"},\"status\":400}" + "{\"error\":{\"root_cause\":[{\"type\":\"not_x_content_exception\",\"reason\":\"Compressor detection can only be called on some xcontent bytes or" + + " compressed xcontent bytes\"}],\"type\":\"not_x_content_exception\",\"reason\":\"Compressor detection can only be called on some xcontent " + + "bytes or compressed xcontent bytes\"},\"status\":400}", + channel.getRestResponse().content().utf8ToString() + ); + } + + public void testHandleBadInputWithCreateIndexReturnsDetailedErrorStackTraceWhenRequested() { + final FakeRestRequest fakeRestRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withPath("/foo") + .withMethod(RestRequest.Method.PUT) + .withParams(new HashMap<>(Map.of("error_trace", "true"))) + .withContent(new BytesArray("ddd"), MediaTypeRegistry.JSON) + .build(); + final AssertingChannel channel = new AssertingChannel(fakeRestRequest, true, RestStatus.BAD_REQUEST); + restController.registerHandler(RestRequest.Method.PUT, "/foo", new RestCreateIndexAction()); + restController.dispatchRequest(fakeRestRequest, channel, client.threadPool().getThreadContext()); + String responseBodyString = channel.getRestResponse().content().utf8ToString(); + assertThat(channel.detailedErrorStackTraceEnabled(), equalTo(true)); + assertThat( + responseBodyString, + containsString( + "{\"error\":{\"root_cause\":[{\"type\":\"not_x_content_exception\",\"reason\":\"Compressor detection can only be called on some xcontent " + + "bytes or compressed xcontent bytes\"" + ) + ); + assertThat( + responseBodyString, + containsString( + "\"stack_trace\":\"OpenSearchException[Compressor detection can only be called on some xcontent bytes or compressed xcontent bytes];" + ) + ); + assertThat(responseBodyString, containsString("\"status\":400")); + } + + public void testHandleBadInputWithCreateIndexReturnsOnlyErrorSummaryWithDisabledErrorTrace() { + final FakeRestRequest fakeRestRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).withPath("/foo") + .withMethod(RestRequest.Method.PUT) + .withParams(new HashMap<>(Map.of("error_trace", "false"))) + .withContent(new BytesArray("ddd"), MediaTypeRegistry.JSON) + .build(); + final AssertingChannel channel = new AssertingChannel(fakeRestRequest, true, RestStatus.BAD_REQUEST); + restController.registerHandler(RestRequest.Method.PUT, "/foo", new RestCreateIndexAction()); + restController.dispatchRequest(fakeRestRequest, channel, client.threadPool().getThreadContext()); + String responseBodyString = channel.getRestResponse().content().utf8ToString(); + assertThat(channel.detailedErrorStackTraceEnabled(), equalTo(false)); + assertThat(responseBodyString, containsString("\"type\":\"not_x_content_exception\"")); + assertThat( + responseBodyString, + containsString( + "\"reason\":\"Compressor detection can only be called on some xcontent bytes " + "or compressed xcontent bytes\"" + ) ); + assertThat(responseBodyString, not(containsString("stack_trace"))); + assertThat(responseBodyString, containsString("\"status\":400")); } public void testDispatchUnsupportedHttpMethod() {