Skip to content

Commit b30a4b2

Browse files
authored
Output a consistent format when generating error json (#90529)
Now, error fields will always have 'type' and 'reason' fields, and the information in those fields is the same regardless of whether the output is detailed or not
1 parent 0db51d5 commit b30a4b2

32 files changed

+389
-201
lines changed

Diff for: docs/changelog/90529.yaml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
pr: 90529
2+
summary: Output a consistent format when generating error json
3+
area: Infra/REST API
4+
type: "breaking"
5+
issues:
6+
- 89387
7+
breaking:
8+
title: Error JSON structure has changed when detailed errors are disabled
9+
area: REST API
10+
details: |-
11+
This change modifies the JSON format of error messages returned to REST clients
12+
when detailed messages are turned off.
13+
Previously, JSON returned when an exception occurred, and `http.detailed_errors.enabled: false` was set,
14+
just consisted of a single `"error"` text field with some basic information.
15+
Setting `http.detailed_errors.enabled: true` (the default) changed this field
16+
to an object with more detailed information.
17+
With this change, non-detailed errors now have the same structure as detailed errors. `"error"` will now always
18+
be an object with, at a minimum, a `"type"` and `"reason"` field. Additional fields are included when detailed
19+
errors are enabled.
20+
To use the previous structure for non-detailed errors, use the v8 REST API.
21+
impact: |-
22+
If you have set `http.detailed_errors.enabled: false` (the default is `true`)
23+
the structure of JSON when any exceptions occur now matches the structure when
24+
detailed errors are enabled.
25+
To use the previous structure for non-detailed errors, use the v8 REST API.
26+
notable: false

Diff for: docs/reference/modules/http.asciidoc

+3-5
Original file line numberDiff line numberDiff line change
@@ -145,11 +145,9 @@ NOTE: This header is only returned when the setting is set to `true`.
145145

146146
`http.detailed_errors.enabled`::
147147
(<<static-cluster-setting,Static>>, boolean)
148-
Configures whether detailed error reporting in HTTP responses is enabled.
149-
Defaults to `true`, which means that HTTP requests that include the
150-
<<common-options-error-options,`?error_trace` parameter>> will return a
151-
detailed error message including a stack trace if they encounter an exception.
152-
If set to `false`, requests with the `?error_trace` parameter are rejected.
148+
Configures whether detailed error reporting in HTTP responses is enabled. Defaults to `true`.
149+
When this option is set to `false`, only basic information is returned if an error occurs in the request,
150+
and requests with <<common-options-error-options,`?error_trace` parameter>> set are rejected.
153151

154152
`http.pipelining.max_events`::
155153
(<<static-cluster-setting,Static>>, integer)

Diff for: server/src/main/java/org/elasticsearch/ElasticsearchException.java

+39-15
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException;
2626
import org.elasticsearch.core.CheckedFunction;
2727
import org.elasticsearch.core.Nullable;
28+
import org.elasticsearch.core.RestApiVersion;
2829
import org.elasticsearch.core.Tuple;
30+
import org.elasticsearch.core.UpdateForV10;
2931
import org.elasticsearch.health.node.action.HealthNodeNotDiscoveredException;
3032
import org.elasticsearch.index.Index;
3133
import org.elasticsearch.index.mapper.DocumentParsingException;
@@ -611,23 +613,31 @@ protected static void generateThrowableXContent(XContentBuilder builder, Params
611613
*/
612614
public static XContentBuilder generateFailureXContent(XContentBuilder builder, Params params, @Nullable Exception e, boolean detailed)
613615
throws IOException {
614-
// No exception to render as an error
616+
if (builder.getRestApiVersion() == RestApiVersion.V_8) {
617+
if (e == null) {
618+
return builder.field(ERROR, "unknown");
619+
}
620+
if (detailed == false) {
621+
return generateNonDetailedFailureXContentV8(builder, e);
622+
}
623+
// else fallthrough
624+
}
625+
615626
if (e == null) {
616-
return builder.field(ERROR, "unknown");
627+
// No exception to render as an error
628+
builder.startObject(ERROR);
629+
builder.field(TYPE, "unknown");
630+
builder.field(REASON, "unknown");
631+
return builder.endObject();
617632
}
618633

619-
// Render the exception with a simple message
620634
if (detailed == false) {
621-
String message = "No ElasticsearchException found";
622-
Throwable t = e;
623-
for (int counter = 0; counter < 10 && t != null; counter++) {
624-
if (t instanceof ElasticsearchException) {
625-
message = t.getClass().getSimpleName() + "[" + t.getMessage() + "]";
626-
break;
627-
}
628-
t = t.getCause();
629-
}
630-
return builder.field(ERROR, message);
635+
// just render the type & message
636+
Throwable t = ExceptionsHelper.unwrapCause(e);
637+
builder.startObject(ERROR);
638+
builder.field(TYPE, getExceptionName(t));
639+
builder.field(REASON, t.getMessage());
640+
return builder.endObject();
631641
}
632642

633643
// Render the exception with all details
@@ -646,6 +656,20 @@ public static XContentBuilder generateFailureXContent(XContentBuilder builder, P
646656
return builder.endObject();
647657
}
648658

659+
@UpdateForV10(owner = UpdateForV10.Owner.CORE_INFRA) // remove V8 API
660+
private static XContentBuilder generateNonDetailedFailureXContentV8(XContentBuilder builder, @Nullable Exception e) throws IOException {
661+
String message = "No ElasticsearchException found";
662+
Throwable t = e;
663+
for (int counter = 0; counter < 10 && t != null; counter++) {
664+
if (t instanceof ElasticsearchException) {
665+
message = t.getClass().getSimpleName() + "[" + t.getMessage() + "]";
666+
break;
667+
}
668+
t = t.getCause();
669+
}
670+
return builder.field(ERROR, message);
671+
}
672+
649673
/**
650674
* Parses the output of {@link #generateFailureXContent(XContentBuilder, Params, Exception, boolean)}
651675
*/
@@ -729,8 +753,8 @@ public static String getExceptionName(Throwable ex) {
729753

730754
static String buildMessage(String type, String reason, String stack) {
731755
StringBuilder message = new StringBuilder("Elasticsearch exception [");
732-
message.append(TYPE).append('=').append(type).append(", ");
733-
message.append(REASON).append('=').append(reason);
756+
message.append(TYPE).append('=').append(type);
757+
message.append(", ").append(REASON).append('=').append(reason);
734758
if (stack != null) {
735759
message.append(", ").append(STACK_TRACE).append('=').append(stack);
736760
}

Diff for: server/src/main/java/org/elasticsearch/common/xcontent/XContentHelper.java

+17-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import org.elasticsearch.common.io.stream.StreamOutput;
2121
import org.elasticsearch.core.CheckedFunction;
2222
import org.elasticsearch.core.Nullable;
23+
import org.elasticsearch.core.RestApiVersion;
2324
import org.elasticsearch.core.Tuple;
2425
import org.elasticsearch.plugins.internal.XContentParserDecorator;
2526
import org.elasticsearch.xcontent.DeprecationHandler;
@@ -626,7 +627,22 @@ public static BytesReference toXContent(ChunkedToXContent toXContent, XContentTy
626627
*/
627628
public static BytesReference toXContent(ToXContent toXContent, XContentType xContentType, Params params, boolean humanReadable)
628629
throws IOException {
629-
try (XContentBuilder builder = XContentBuilder.builder(xContentType.xContent())) {
630+
return toXContent(toXContent, xContentType, RestApiVersion.current(), params, humanReadable);
631+
}
632+
633+
/**
634+
* Returns the bytes that represent the XContent output of the provided {@link ToXContent} object, using the provided
635+
* {@link XContentType}. Wraps the output into a new anonymous object according to the value returned
636+
* by the {@link ToXContent#isFragment()} method returns.
637+
*/
638+
public static BytesReference toXContent(
639+
ToXContent toXContent,
640+
XContentType xContentType,
641+
RestApiVersion restApiVersion,
642+
Params params,
643+
boolean humanReadable
644+
) throws IOException {
645+
try (XContentBuilder builder = XContentBuilder.builder(xContentType.xContent(), restApiVersion)) {
630646
builder.humanReadable(humanReadable);
631647
if (toXContent.isFragment()) {
632648
builder.startObject();

Diff for: server/src/main/java/org/elasticsearch/rest/RestResponse.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.elasticsearch.core.Nullable;
2323
import org.elasticsearch.core.Releasable;
2424
import org.elasticsearch.core.Releasables;
25+
import org.elasticsearch.core.RestApiVersion;
2526
import org.elasticsearch.xcontent.ToXContent;
2627
import org.elasticsearch.xcontent.XContentBuilder;
2728

@@ -146,12 +147,12 @@ public RestResponse(RestChannel channel, RestStatus status, Exception e) throws
146147
params = new ToXContent.DelegatingMapParams(singletonMap(REST_EXCEPTION_SKIP_STACK_TRACE, "false"), params);
147148
}
148149

149-
if (channel.detailedErrorsEnabled() == false) {
150+
if (channel.request().getRestApiVersion() == RestApiVersion.V_8 && channel.detailedErrorsEnabled() == false) {
150151
deprecationLogger.warn(
151152
DeprecationCategory.API,
152153
"http_detailed_errors",
153-
"The JSON format of non-detailed errors will change in Elasticsearch 9.0 to match the JSON structure"
154-
+ " used for detailed errors. To keep using the existing format, use the V8 REST API."
154+
"The JSON format of non-detailed errors has changed in Elasticsearch 9.0 to match the JSON structure"
155+
+ " used for detailed errors."
155156
);
156157
}
157158

0 commit comments

Comments
 (0)