Skip to content

Commit 334323f

Browse files
github-actions[bot]l-trottaswallez
authored
Feature: option to retrieve original json body if parse exception occurred (#886) (#897)
* base working impl * twr, remove unused constructor * unit test * license * complete refactor * reverting old changes * codestyle * leave ElasticsearchTransportBase untouched * Update java-client/src/main/java/co/elastic/clients/transport/TransportOptions.java * Update java-client/src/main/java/co/elastic/clients/transport/TransportOptions.java * custom generic response, checking in transport base * license header * asciidocs --------- Co-authored-by: Laura Trotta <153528055+l-trotta@users.noreply.github.com> Co-authored-by: Sylvain Wallez <sylvain@elastic.co>
1 parent 164dca0 commit 334323f

File tree

10 files changed

+262
-12
lines changed

10 files changed

+262
-12
lines changed

Diff for: docs/release-notes/release-highlights.asciidoc

+20
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ For a list of detailed changes, including bug fixes, please see the https://gith
88
[discrete]
99
==== Version 8.16
1010
* `ElasticsearchClient` is now `Closeable`. Closing a client object also closes the underlying transport - https://github.com/elastic/elasticsearch-java/pull/851[#851]
11+
* Added option to make the response body available in case of deserialization error- https://github.com/elastic/elasticsearch-java/pull/886[#886].
12+
13+
** While it has always been possible to set the log level to `trace` and have the client print both the json bodies of the requests and responses, it's often not the best solution because of the large amount of information printed.
14+
** To enable the feature:
15+
16+
RestClientOptions options = new RestClientOptions(RequestOptions.DEFAULT, true);
17+
ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper(), options);
18+
ElasticsearchClient esClientWithOptions = new ElasticsearchClient(transport);
19+
20+
** To retrieve the original body from the TransportException that gets thrown in case of deserialization errors:
21+
22+
try{
23+
// some code that returns faulty json
24+
}
25+
catch (TransportException ex){
26+
try (RepeatableBodyResponse repeatableResponse = (RepeatableBodyResponse) ex.response()) {
27+
BinaryData body = repeatableResponse.body();
28+
}
29+
}
30+
1131

1232
[discrete]
1333
==== Version 8.15

Diff for: java-client/src/main/java/co/elastic/clients/transport/DefaultTransportOptions.java

+26-1
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,24 @@ public class DefaultTransportOptions implements TransportOptions {
3737
private final HeaderMap headers;
3838
private final Map<String, String> parameters;
3939
private final Function<List<String>, Boolean> onWarnings;
40+
private boolean keepResponseBodyOnException;
4041

4142
public static final DefaultTransportOptions EMPTY = new DefaultTransportOptions();
4243

4344
public DefaultTransportOptions() {
4445
this(new HeaderMap(), Collections.emptyMap(), null);
4546
}
4647

48+
public DefaultTransportOptions(
49+
@Nullable HeaderMap headers,
50+
@Nullable Map<String, String> parameters,
51+
@Nullable Function<List<String>, Boolean> onWarnings,
52+
boolean keepResponseBodyOnException
53+
) {
54+
this(headers,parameters,onWarnings);
55+
this.keepResponseBodyOnException = keepResponseBodyOnException;
56+
}
57+
4758
public DefaultTransportOptions(
4859
@Nullable HeaderMap headers,
4960
@Nullable Map<String, String> parameters,
@@ -53,10 +64,11 @@ public DefaultTransportOptions(
5364
this.parameters = (parameters == null || parameters.isEmpty()) ?
5465
Collections.emptyMap() : Collections.unmodifiableMap(parameters);
5566
this.onWarnings = onWarnings;
67+
this.keepResponseBodyOnException = false;
5668
}
5769

5870
protected DefaultTransportOptions(AbstractBuilder<?> builder) {
59-
this(builder.headers, builder.parameters, builder.onWarnings);
71+
this(builder.headers, builder.parameters, builder.onWarnings, builder.keepResponseBodyOnException);
6072
}
6173

6274
public static DefaultTransportOptions of(@Nullable TransportOptions options) {
@@ -88,6 +100,11 @@ public Function<List<String>, Boolean> onWarnings() {
88100
return onWarnings;
89101
}
90102

103+
@Override
104+
public boolean keepResponseBodyOnException() {
105+
return keepResponseBodyOnException;
106+
}
107+
91108
@Override
92109
public Builder toBuilder() {
93110
return new Builder(this);
@@ -111,6 +128,7 @@ public abstract static class AbstractBuilder<BuilderT extends AbstractBuilder<Bu
111128
private HeaderMap headers;
112129
private Map<String, String> parameters;
113130
private Function<List<String>, Boolean> onWarnings;
131+
private boolean keepResponseBodyOnException;
114132

115133
public AbstractBuilder() {
116134
}
@@ -119,10 +137,17 @@ public AbstractBuilder(DefaultTransportOptions options) {
119137
this.headers = new HeaderMap(options.headers);
120138
this.parameters = copyOrNull(options.parameters);
121139
this.onWarnings = options.onWarnings;
140+
this.keepResponseBodyOnException = options.keepResponseBodyOnException;
122141
}
123142

124143
protected abstract BuilderT self();
125144

145+
@Override
146+
public BuilderT keepResponseBodyOnException(boolean value) {
147+
this.keepResponseBodyOnException = value;
148+
return self();
149+
}
150+
126151
@Override
127152
public BuilderT addHeader(String name, String value) {
128153
if (name.equalsIgnoreCase(HeaderMap.CLIENT_META)) {

Diff for: java-client/src/main/java/co/elastic/clients/transport/ElasticsearchTransportBase.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,15 @@
2929
import co.elastic.clients.transport.endpoints.BooleanEndpoint;
3030
import co.elastic.clients.transport.endpoints.BooleanResponse;
3131
import co.elastic.clients.transport.http.HeaderMap;
32+
import co.elastic.clients.transport.http.RepeatableBodyResponse;
3233
import co.elastic.clients.transport.http.TransportHttpClient;
3334
import co.elastic.clients.transport.instrumentation.Instrumentation;
3435
import co.elastic.clients.transport.instrumentation.NoopInstrumentation;
3536
import co.elastic.clients.transport.instrumentation.OpenTelemetryForElasticsearch;
37+
import co.elastic.clients.util.ByteArrayBinaryData;
3638
import co.elastic.clients.util.LanguageRuntimeVersions;
3739
import co.elastic.clients.util.ApiTypeHelper;
3840
import co.elastic.clients.util.BinaryData;
39-
import co.elastic.clients.util.ByteArrayBinaryData;
4041
import co.elastic.clients.util.ContentType;
4142
import co.elastic.clients.util.MissingRequiredPropertyException;
4243
import co.elastic.clients.util.NoCopyByteArrayOutputStream;
@@ -306,6 +307,9 @@ private <ResponseT, ErrorT> ResponseT getApiResponse(
306307

307308
int statusCode = clientResp.statusCode();
308309

310+
if(options().keepResponseBodyOnException()){
311+
clientResp = RepeatableBodyResponse.of(clientResp);
312+
}
309313
try {
310314
if (statusCode == 200) {
311315
checkProductHeader(clientResp, endpoint);
@@ -377,6 +381,7 @@ private <ResponseT> ResponseT decodeTransportResponse(
377381
) throws IOException {
378382

379383
if (endpoint instanceof JsonEndpoint) {
384+
380385
@SuppressWarnings("unchecked")
381386
JsonEndpoint<?, ResponseT, ?> jsonEndpoint = (JsonEndpoint<?, ResponseT, ?>) endpoint;
382387
// Successful response

Diff for: java-client/src/main/java/co/elastic/clients/transport/TransportOptions.java

+14
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ public interface TransportOptions {
3838

3939
Function<List<String>, Boolean> onWarnings();
4040

41+
/**
42+
* If {@code true}, the response body in {@code TransportException.response().body()} is guaranteed to be
43+
* replayable (i.e. buffered), even if the original response was streamed. This allows inspecting the
44+
* response body in case of error.
45+
*/
46+
boolean keepResponseBodyOnException();
47+
4148
Builder toBuilder();
4249

4350
default TransportOptions with(Consumer<Builder> fn) {
@@ -59,5 +66,12 @@ interface Builder extends ObjectBuilder<TransportOptions> {
5966
Builder removeParameter(String name);
6067

6168
Builder onWarnings(Function<List<String>, Boolean> listener);
69+
70+
/**
71+
* Should the response body be buffered and made available in {@code TransportException.response().body()}?
72+
* This setting guarantees that the response body is buffered for inspection if parsing fails, even if originally
73+
* streamed by the http library.
74+
*/
75+
Builder keepResponseBodyOnException(boolean value);
6276
}
6377
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package co.elastic.clients.transport.http;
21+
22+
import co.elastic.clients.util.BinaryData;
23+
import co.elastic.clients.util.ByteArrayBinaryData;
24+
25+
import javax.annotation.Nullable;
26+
import java.io.IOException;
27+
import java.util.List;
28+
29+
public class RepeatableBodyResponse implements TransportHttpClient.Response {
30+
31+
private final TransportHttpClient.Response response;
32+
private final BinaryData body;
33+
34+
public static TransportHttpClient.Response of(TransportHttpClient.Response response) throws IOException {
35+
BinaryData body = response.body();
36+
if (body == null || body.isRepeatable()) {
37+
return response;
38+
}
39+
return new RepeatableBodyResponse(response);
40+
}
41+
42+
public RepeatableBodyResponse(TransportHttpClient.Response response) throws IOException {
43+
this.response = response;
44+
this.body = new ByteArrayBinaryData(response.body());
45+
}
46+
47+
@Override
48+
public TransportHttpClient.Node node() {
49+
return response.node();
50+
}
51+
52+
@Override
53+
public int statusCode() {
54+
return response.statusCode();
55+
}
56+
57+
@Nullable
58+
@Override
59+
public String header(String name) {
60+
return response.header(name);
61+
}
62+
63+
@Override
64+
public List<String> headers(String name) {
65+
return response.headers(name);
66+
}
67+
68+
@Nullable
69+
@Override
70+
public BinaryData body() throws IOException {
71+
return this.body;
72+
}
73+
74+
@Nullable
75+
@Override
76+
public Object originalResponse() {
77+
return response.originalResponse();
78+
}
79+
80+
@Override
81+
public void close() throws IOException {
82+
response.close();
83+
}
84+
}

Diff for: java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientHttpClient.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ public RestClientOptions createOptions(@Nullable TransportOptions options) {
8585
}
8686

8787
@Override
88-
public Response performRequest(String endpointId, @Nullable Node node, Request request, TransportOptions options) throws IOException {
88+
public Response performRequest(String endpointId, @Nullable Node node, Request request,
89+
TransportOptions options) throws IOException {
8990
RestClientOptions rcOptions = RestClientOptions.of(options);
9091
org.elasticsearch.client.Request restRequest = createRestRequest(request, rcOptions);
9192
org.elasticsearch.client.Response restResponse = restClient.performRequest(restRequest);
@@ -103,7 +104,7 @@ public CompletableFuture<Response> performRequestAsync(
103104
try {
104105
RestClientOptions rcOptions = RestClientOptions.of(options);
105106
restRequest = createRestRequest(request, rcOptions);
106-
} catch(Throwable thr) {
107+
} catch (Throwable thr) {
107108
// Terminate early
108109
future.completeExceptionally(thr);
109110
return future;
@@ -166,7 +167,7 @@ private org.elasticsearch.client.Request createRestRequest(Request request, Rest
166167
if (body != null) {
167168
ContentType ct = null;
168169
String ctStr;
169-
if (( ctStr = requestHeaders.get(HeaderMap.CONTENT_TYPE)) != null) {
170+
if ((ctStr = requestHeaders.get(HeaderMap.CONTENT_TYPE)) != null) {
170171
ct = ContentTypeCache.computeIfAbsent(ctStr, ContentType::parse);
171172
}
172173
clientReq.setEntity(new MultiBufferEntity(body, ct));

Diff for: java-client/src/main/java/co/elastic/clients/transport/rest_client/RestClientOptions.java

+19-3
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ public class RestClientOptions implements TransportOptions {
4242

4343
private final RequestOptions options;
4444

45+
boolean keepResponseBodyOnException;
46+
4547
@VisibleForTesting
4648
static final String CLIENT_META_VALUE = getClientMeta();
4749
@VisibleForTesting
@@ -63,7 +65,8 @@ static RestClientOptions of(@Nullable TransportOptions options) {
6365
return builder.build();
6466
}
6567

66-
public RestClientOptions(RequestOptions options) {
68+
public RestClientOptions(RequestOptions options, boolean keepResponseBodyOnException) {
69+
this.keepResponseBodyOnException = keepResponseBodyOnException;
6770
this.options = addBuiltinHeaders(options.toBuilder()).build();
6871
}
6972

@@ -99,6 +102,11 @@ public Function<List<String>, Boolean> onWarnings() {
99102
return warnings -> options.getWarningsHandler().warningsShouldFailRequest(warnings);
100103
}
101104

105+
@Override
106+
public boolean keepResponseBodyOnException() {
107+
return this.keepResponseBodyOnException;
108+
}
109+
102110
@Override
103111
public Builder toBuilder() {
104112
return new Builder(options.toBuilder());
@@ -108,6 +116,8 @@ public static class Builder implements TransportOptions.Builder {
108116

109117
private RequestOptions.Builder builder;
110118

119+
private boolean keepResponseBodyOnException;
120+
111121
public Builder(RequestOptions.Builder builder) {
112122
this.builder = builder;
113123
}
@@ -181,14 +191,20 @@ public TransportOptions.Builder onWarnings(Function<List<String>, Boolean> liste
181191
return this;
182192
}
183193

194+
@Override
195+
public TransportOptions.Builder keepResponseBodyOnException(boolean value) {
196+
this.keepResponseBodyOnException = value;
197+
return this;
198+
}
199+
184200
@Override
185201
public RestClientOptions build() {
186-
return new RestClientOptions(addBuiltinHeaders(builder).build());
202+
return new RestClientOptions(addBuiltinHeaders(builder).build(), keepResponseBodyOnException);
187203
}
188204
}
189205

190206
static RestClientOptions initialOptions() {
191-
return new RestClientOptions(SafeResponseConsumer.DEFAULT_REQUEST_OPTIONS);
207+
return new RestClientOptions(SafeResponseConsumer.DEFAULT_REQUEST_OPTIONS, false);
192208
}
193209

194210
private static RequestOptions.Builder addBuiltinHeaders(RequestOptions.Builder builder) {

Diff for: java-client/src/test/java/co/elastic/clients/documentation/DocTestsTransport.java

+5
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ public Function<List<String>, Boolean> onWarnings() {
6464
return null;
6565
}
6666

67+
@Override
68+
public boolean keepResponseBodyOnException() {
69+
return false;
70+
}
71+
6772
@Override
6873
public Builder toBuilder() {
6974
return null;

0 commit comments

Comments
 (0)