Skip to content
15 changes: 11 additions & 4 deletions server/src/main/java/org/elasticsearch/ElasticsearchException.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ public class ElasticsearchException extends RuntimeException implements ToXConte
private static final String ROOT_CAUSE = "root_cause";

static final String TIMED_OUT_HEADER = "X-Timed-Out";
static final String EXCEPTION_TYPE_HEADER = "X-Elasticsearch-Exception";

private static final Map<Integer, CheckedFunction<StreamInput, ? extends ElasticsearchException, IOException>> ID_TO_SUPPLIER;
private static final Map<Class<? extends ElasticsearchException>, ElasticsearchExceptionHandle> CLASS_TO_ELASTICSEARCH_EXCEPTION_HANDLE;
Expand All @@ -130,7 +131,7 @@ public class ElasticsearchException extends RuntimeException implements ToXConte
@SuppressWarnings("this-escape")
public ElasticsearchException(Throwable cause) {
super(cause);
maybePutTimeoutHeader();
addErrorHeaders();
}

/**
Expand All @@ -145,7 +146,7 @@ public ElasticsearchException(Throwable cause) {
@SuppressWarnings("this-escape")
public ElasticsearchException(String msg, Object... args) {
super(LoggerMessageFormat.format(msg, args));
maybePutTimeoutHeader();
addErrorHeaders();
}

/**
Expand All @@ -162,7 +163,7 @@ public ElasticsearchException(String msg, Object... args) {
@SuppressWarnings("this-escape")
public ElasticsearchException(String msg, Throwable cause, Object... args) {
super(LoggerMessageFormat.format(msg, args), cause);
maybePutTimeoutHeader();
addErrorHeaders();
}

@SuppressWarnings("this-escape")
Expand All @@ -173,11 +174,17 @@ public ElasticsearchException(StreamInput in) throws IOException {
metadata.putAll(in.readMapOfLists(StreamInput::readString));
}

private void maybePutTimeoutHeader() {
private void addErrorHeaders() {
if (isTimeout()) {
// see https://www.rfc-editor.org/rfc/rfc8941.html#section-4.1.9 for booleans in structured headers
headers.put(TIMED_OUT_HEADER, List.of("?1"));
}
// TODO: cache unwrapping the cause? we do this in several places...
Throwable cause = unwrapCause();
RestStatus status = ExceptionsHelper.status(cause);
if (status.getStatus() >= 500) {
headers.put(EXCEPTION_TYPE_HEADER, List.of(cause.getClass().getSimpleName()));
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1553,19 +1553,24 @@ private void testExceptionLoop(Exception rootException) throws IOException {
assertArrayEquals(ser.getStackTrace(), rootException.getStackTrace());
}

static class ExceptionSubclass extends ElasticsearchException {
static class TimeoutSubclass extends ElasticsearchException {
TimeoutSubclass(String message) {
super(message);
}

@Override
public boolean isTimeout() {
return true;
}

ExceptionSubclass(String message) {
super(message);
@Override
public RestStatus status() {
return RestStatus.BAD_REQUEST;
}
}

public void testTimeout() throws IOException {
var e = new ExceptionSubclass("some timeout");
public void testTimeoutHeader() throws IOException {
var e = new TimeoutSubclass("some timeout");
assertThat(e.getHeaderKeys(), hasItem(ElasticsearchException.TIMED_OUT_HEADER));

XContentBuilder builder = XContentFactory.jsonBuilder();
Expand All @@ -1574,7 +1579,7 @@ public void testTimeout() throws IOException {
builder.endObject();
String expected = """
{
"type": "exception_subclass",
"type": "timeout_subclass",
"reason": "some timeout",
"timed_out": true,
"header": {
Expand All @@ -1583,4 +1588,61 @@ public void testTimeout() throws IOException {
}""";
assertEquals(XContentHelper.stripWhitespace(expected), Strings.toString(builder));
}

static class Exception5xx extends ElasticsearchException {
Exception5xx(String message) {
super(message);
}

@Override
public RestStatus status() {
return RestStatus.INTERNAL_SERVER_ERROR;
}
}

static class Exception4xx extends ElasticsearchException {
Exception4xx(String message) {
super(message);
}

@Override
public RestStatus status() {
return RestStatus.BAD_REQUEST;
}
}

public void testExceptionTypeHeader() throws IOException {
var e = new Exception5xx("some exception");
assertThat(e.getHeaderKeys(), hasItem(ElasticsearchException.EXCEPTION_TYPE_HEADER));

XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
e.toXContent(builder, ToXContent.EMPTY_PARAMS);
builder.endObject();
String expected = """
{
"type": "exception5xx",
"reason": "some exception",
"header": {
"X-Elasticsearch-Exception": "Exception5xx"
}
}""";
assertEquals(XContentHelper.stripWhitespace(expected), Strings.toString(builder));
}

public void testNoExceptionTypeHeaderOn4xx() throws IOException {
var e = new Exception4xx("some exception");
assertThat(e.getHeaderKeys(), not(hasItem(ElasticsearchException.EXCEPTION_TYPE_HEADER)));

XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
e.toXContent(builder, ToXContent.EMPTY_PARAMS);
builder.endObject();
String expected = """
{
"type": "exception4xx",
"reason": "some exception"
}""";
assertEquals(XContentHelper.stripWhitespace(expected), Strings.toString(builder));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public void testExceptionRegistration() throws IOException, URISyntaxException {
CancellableThreadsTests.CustomException.class,
RestResponseTests.WithHeadersException.class,
AbstractClientHeadersTestCase.InternalException.class,
ElasticsearchExceptionTests.ExceptionSubclass.class
ElasticsearchExceptionTests.TimeoutSubclass.class
);
FileVisitor<Path> visitor = new FileVisitor<Path>() {
private Path pkgPrefix = PathUtils.get(path).getParent();
Expand Down