diff --git a/core/src/main/java/org/elasticsearch/ElasticsearchException.java b/core/src/main/java/org/elasticsearch/ElasticsearchException.java index 99d76313338..2a587a371d5 100644 --- a/core/src/main/java/org/elasticsearch/ElasticsearchException.java +++ b/core/src/main/java/org/elasticsearch/ElasticsearchException.java @@ -565,7 +565,7 @@ public class ElasticsearchException extends RuntimeException implements ToXConte * Parses the output of {@link #generateFailureXContent(XContentBuilder, Params, Exception, boolean)} */ public static ElasticsearchException failureFromXContent(XContentParser parser) throws IOException { - XContentParser.Token token = parser.nextToken(); + XContentParser.Token token = parser.currentToken(); ensureFieldName(parser, token, ERROR); token = parser.nextToken(); diff --git a/core/src/main/java/org/elasticsearch/rest/BytesRestResponse.java b/core/src/main/java/org/elasticsearch/rest/BytesRestResponse.java index 5abaf0d6c13..2d45bc1765b 100644 --- a/core/src/main/java/org/elasticsearch/rest/BytesRestResponse.java +++ b/core/src/main/java/org/elasticsearch/rest/BytesRestResponse.java @@ -23,24 +23,29 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.apache.logging.log4j.util.Supplier; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import static java.util.Collections.singletonMap; import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE_DEFAULT; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; public class BytesRestResponse extends RestResponse { public static final String TEXT_CONTENT_TYPE = "text/plain; charset=UTF-8"; + private static final String STATUS = "status"; + private final RestStatus status; private final BytesReference content; private final String contentType; @@ -136,8 +141,44 @@ public class BytesRestResponse extends RestResponse { XContentBuilder builder = channel.newErrorBuilder().startObject(); ElasticsearchException.generateFailureXContent(builder, params, e, channel.detailedErrorsEnabled()); - builder.field("status", status.getStatus()); + builder.field(STATUS, status.getStatus()); builder.endObject(); return builder; } + + public static ElasticsearchStatusException errorFromXContent(XContentParser parser) throws IOException { + XContentParser.Token token = parser.nextToken(); + ensureExpectedToken(XContentParser.Token.START_OBJECT, token, parser::getTokenLocation); + + ElasticsearchException exception = null; + RestStatus status = null; + + String currentFieldName = null; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } + if (STATUS.equals(currentFieldName)) { + if (token != XContentParser.Token.FIELD_NAME) { + ensureExpectedToken(XContentParser.Token.VALUE_NUMBER, token, parser::getTokenLocation); + status = RestStatus.fromCode(parser.intValue()); + } + } else { + exception = ElasticsearchException.failureFromXContent(parser); + } + } + + if (exception == null) { + throw new IllegalStateException("Failed to parse elasticsearch status exception: no exception was found"); + } + + ElasticsearchStatusException result = new ElasticsearchStatusException(exception.getMessage(), status, exception.getCause()); + for (String header : exception.getHeaderKeys()) { + result.addHeader(header, exception.getHeader(header)); + } + for (String metadata : exception.getMetadataKeys()) { + result.addMetadata(metadata, exception.getMetadata(metadata)); + } + return result; + } } diff --git a/core/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java b/core/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java index 9fba12affa7..4dbadfeb530 100644 --- a/core/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java +++ b/core/src/test/java/org/elasticsearch/ElasticsearchExceptionTests.java @@ -604,6 +604,7 @@ public class ElasticsearchExceptionTests extends ESTestCase { ElasticsearchException parsedFailure; try (XContentParser parser = createParser(xContent, failureBytes)) { assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); parsedFailure = ElasticsearchException.failureFromXContent(parser); assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); assertNull(parser.nextToken()); @@ -629,6 +630,7 @@ public class ElasticsearchExceptionTests extends ESTestCase { ElasticsearchException parsedFailure; try (XContentParser parser = createParser(xContent, failureBytes)) { assertEquals(XContentParser.Token.START_OBJECT, parser.nextToken()); + assertEquals(XContentParser.Token.FIELD_NAME, parser.nextToken()); parsedFailure = ElasticsearchException.failureFromXContent(parser); assertEquals(XContentParser.Token.END_OBJECT, parser.nextToken()); assertNull(parser.nextToken()); @@ -670,14 +672,14 @@ public class ElasticsearchExceptionTests extends ESTestCase { }, expectedJson); } - private static void assertDeepEquals(ElasticsearchException expected, ElasticsearchException actual) { - if (expected == null) { - assertNull(actual); - } else { - assertNotNull(actual); - } - + public static void assertDeepEquals(ElasticsearchException expected, ElasticsearchException actual) { do { + if (expected == null) { + assertNull(actual); + } else { + assertNotNull(actual); + } + assertEquals(expected.getMessage(), actual.getMessage()); assertEquals(expected.getHeaders(), actual.getHeaders()); assertEquals(expected.getMetadata(), actual.getMetadata()); diff --git a/core/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java b/core/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java index 4546a0fa112..09f4f552b7c 100644 --- a/core/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java +++ b/core/src/test/java/org/elasticsearch/rest/BytesRestResponseTests.java @@ -20,12 +20,19 @@ package org.elasticsearch.rest; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ExceptionsHelper; +import org.elasticsearch.ResourceAlreadyExistsException; +import org.elasticsearch.ResourceNotFoundException; import org.elasticsearch.action.search.SearchPhaseExecutionException; import org.elasticsearch.action.search.ShardSearchFailure; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.Index; import org.elasticsearch.search.SearchShardTarget; import org.elasticsearch.test.ESTestCase; @@ -37,6 +44,7 @@ import java.io.IOException; import java.util.Collections; import java.util.Map; +import static org.elasticsearch.ElasticsearchExceptionTests.assertDeepEquals; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.not; @@ -210,6 +218,144 @@ public class BytesRestResponseTests extends ESTestCase { assertThat(content, containsString("\"status\":" + 500)); } + public void testErrorToAndFromXContent() throws IOException { + final boolean detailed = randomBoolean(); + + Exception original; + ElasticsearchException cause = null; + String reason; + String type = "exception"; + RestStatus status = RestStatus.INTERNAL_SERVER_ERROR; + boolean addHeadersOrMetadata = false; + + switch (randomIntBetween(0, 5)) { + case 0: + original = new ElasticsearchException("ElasticsearchException without cause"); + if (detailed) { + addHeadersOrMetadata = randomBoolean(); + reason = "ElasticsearchException without cause"; + } else { + reason = "ElasticsearchException[ElasticsearchException without cause]"; + } + break; + case 1: + original = new ElasticsearchException("ElasticsearchException with a cause", new FileNotFoundException("missing")); + if (detailed) { + addHeadersOrMetadata = randomBoolean(); + type = "exception"; + reason = "ElasticsearchException with a cause"; + cause = new ElasticsearchException("Elasticsearch exception [type=file_not_found_exception, reason=missing]"); + } else { + reason = "ElasticsearchException[ElasticsearchException with a cause]"; + } + break; + case 2: + original = new ResourceNotFoundException("ElasticsearchException with custom status"); + status = RestStatus.NOT_FOUND; + if (detailed) { + addHeadersOrMetadata = randomBoolean(); + type = "resource_not_found_exception"; + reason = "ElasticsearchException with custom status"; + } else { + reason = "ResourceNotFoundException[ElasticsearchException with custom status]"; + } + break; + case 3: + TransportAddress address = buildNewFakeTransportAddress(); + original = new RemoteTransportException("remote", address, "action", + new ResourceAlreadyExistsException("ElasticsearchWrapperException with a cause that has a custom status")); + status = RestStatus.BAD_REQUEST; + if (detailed) { + type = "resource_already_exists_exception"; + reason = "ElasticsearchWrapperException with a cause that has a custom status"; + } else { + reason = "RemoteTransportException[[remote][" + address.toString() + "][action]]"; + } + break; + case 4: + original = new RemoteTransportException("ElasticsearchWrapperException with a cause that has a special treatment", + new IllegalArgumentException("wrong")); + status = RestStatus.BAD_REQUEST; + if (detailed) { + type = "illegal_argument_exception"; + reason = "wrong"; + } else { + reason = "RemoteTransportException[[ElasticsearchWrapperException with a cause that has a special treatment]]"; + } + break; + case 5: + status = randomFrom(RestStatus.values()); + original = new ElasticsearchStatusException("ElasticsearchStatusException with random status", status); + if (detailed) { + addHeadersOrMetadata = randomBoolean(); + type = "status_exception"; + reason = "ElasticsearchStatusException with random status"; + } else { + reason = "ElasticsearchStatusException[ElasticsearchStatusException with random status]"; + } + break; + default: + throw new UnsupportedOperationException("Failed to generate random exception"); + } + + String message = "Elasticsearch exception [type=" + type + ", reason=" + reason + "]"; + ElasticsearchStatusException expected = new ElasticsearchStatusException(message, status, cause); + + if (addHeadersOrMetadata) { + ElasticsearchException originalException = ((ElasticsearchException) original); + if (randomBoolean()) { + originalException.addHeader("foo", "bar", "baz"); + expected.addHeader("foo", "bar", "baz"); + } + if (randomBoolean()) { + originalException.addMetadata("es.metadata_0", "0"); + expected.addMetadata("es.metadata_0", "0"); + } + if (randomBoolean()) { + String resourceType = randomAsciiOfLength(5); + String resourceId = randomAsciiOfLength(5); + originalException.setResources(resourceType, resourceId); + expected.setResources(resourceType, resourceId); + } + if (randomBoolean()) { + originalException.setIndex("_index"); + expected.setIndex("_index"); + } + } + + final XContentType xContentType = randomFrom(XContentType.values()); + + Map params = Collections.singletonMap("format", xContentType.mediaType()); + RestRequest request = new FakeRestRequest.Builder(xContentRegistry()).withParams(params).build(); + RestChannel channel = detailed ? new DetailedExceptionRestChannel(request) : new SimpleExceptionRestChannel(request); + + BytesRestResponse response = new BytesRestResponse(channel, original); + + ElasticsearchException parsedError; + try (XContentParser parser = createParser(xContentType.xContent(), response.content())) { + parsedError = BytesRestResponse.errorFromXContent(parser); + assertNull(parser.nextToken()); + } + + assertEquals(expected.status(), parsedError.status()); + assertDeepEquals(expected, parsedError); + } + + public void testNoErrorFromXContent() throws IOException { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> { + try (XContentBuilder builder = XContentBuilder.builder(randomFrom(XContentType.values()).xContent())) { + builder.startObject(); + builder.field("status", randomFrom(RestStatus.values())); + builder.endObject(); + + try (XContentParser parser = createParser(builder.contentType().xContent(), builder.bytes())) { + BytesRestResponse.errorFromXContent(parser); + } + } + }); + assertEquals("Unable to parse elasticsearch status exception", e.getMessage()); + } + public static class WithHeadersException extends ElasticsearchException { WithHeadersException() {