Skip to content

Commit d233863

Browse files
[FEATURE] Raise errors for HTTP error codes in the generic client (#929) (#931)
* [FEATURE] Raise errors for HTTP error codes in the generic client * Address code review comments --------- (cherry picked from commit 5ad54c6) Signed-off-by: Andriy Redko <[email protected]> Signed-off-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent f9724a0 commit d233863

File tree

10 files changed

+296
-61
lines changed

10 files changed

+296
-61
lines changed

Diff for: CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
55
### Added
66
- Add xy_shape property ([#884](https://github.com/opensearch-project/opensearch-java/pull/885))
77
- Add missed fields to MultisearchBody: seqNoPrimaryTerm, storedFields, explain, fields, indicesBoost ([#914](https://github.com/opensearch-project/opensearch-java/pull/914))
8-
- Add OpenSearchGenericClient with support for raw HTTP request/responses ([#910](https://github.com/opensearch-project/opensearch-java/pull/910))
8+
- Add OpenSearchGenericClient with support for raw HTTP request/responses ([#910](https://github.com/opensearch-project/opensearch-java/pull/910), [#929](https://github.com/opensearch-project/opensearch-java/pull/929))
99
- Add missed fields to MultisearchBody: collapse, version, timeout ([#916](https://github.com/opensearch-project/opensearch-java/pull/916)
1010
- Add missed fields to MultisearchBody: ext, rescore and to SearchRequest: ext ([#918](https://github.com/opensearch-project/opensearch-java/pull/918)
1111

Diff for: guides/generic.md

+6
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ The following sample code gets the `OpenSearchGenericClient` from the `OpenSearc
1616
final OpenSearchGenericClient generic = javaClient().generic();
1717
```
1818

19+
The generic client with default options (`ClientOptions.DEFAULT`) returns the responses as those were received from the server. The generic client could be instructed to raise an `OpenSearchClientException` exception instead if the HTTP status code is not indicating the successful response, for example:
20+
21+
```java
22+
final OpenSearchGenericClient generic = javaClient().generic().witClientOptions(ClientOptions.throwOnHttpErrors());
23+
```
24+
1925
## Sending Simple Request
2026
The following sample code sends a simple request that does not require any payload to be provided (typically, `GET` requests).
2127

Diff for: java-client/src/main/java/org/opensearch/client/opensearch/generic/Body.java

+9-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,14 @@ public interface Body extends AutoCloseable {
6767
* @return body as {@link String}
6868
*/
6969
default String bodyAsString() {
70+
return new String(bodyAsBytes(), StandardCharsets.UTF_8);
71+
}
72+
73+
/**
74+
* Gets the body as {@link byte[]}
75+
* @return body as {@link byte[]}
76+
*/
77+
default byte[] bodyAsBytes() {
7078
try (final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
7179
try (final InputStream in = body()) {
7280
final byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
@@ -77,7 +85,7 @@ default String bodyAsString() {
7785
}
7886

7987
out.flush();
80-
return new String(out.toByteArray(), StandardCharsets.UTF_8);
88+
return out.toByteArray();
8189
} catch (final IOException ex) {
8290
throw new UncheckedIOException(ex);
8391
}

Diff for: java-client/src/main/java/org/opensearch/client/opensearch/generic/GenericResponse.java

+4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ final class GenericResponse implements Response {
2727
private final Collection<Map.Entry<String, String>> headers;
2828
private final Body body;
2929

30+
GenericResponse(String uri, String protocol, String method, int status, String reason, Collection<Map.Entry<String, String>> headers) {
31+
this(uri, protocol, method, status, reason, headers, null);
32+
}
33+
3034
GenericResponse(
3135
String uri,
3236
String protocol,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
package org.opensearch.client.opensearch.generic;
10+
11+
/**
12+
* Exception thrown by API client methods when OpenSearch could not accept or
13+
* process a request.
14+
* <p>
15+
* The {@link #response()} contains the the raw response as returned by the API
16+
* endpoint that was called.
17+
*/
18+
public class OpenSearchClientException extends RuntimeException {
19+
20+
private final Response response;
21+
22+
public OpenSearchClientException(Response response) {
23+
super("Request failed: [" + response.getStatus() + "] " + response.getReason());
24+
this.response = response;
25+
}
26+
27+
/**
28+
* The error response sent by OpenSearch
29+
*/
30+
public Response response() {
31+
return this.response;
32+
}
33+
34+
/**
35+
* Status code returned by OpenSearch. Shortcut for
36+
* {@code response().status()}.
37+
*/
38+
public int status() {
39+
return this.response.getStatus();
40+
}
41+
}

Diff for: java-client/src/main/java/org/opensearch/client/opensearch/generic/OpenSearchGenericClient.java

+79-8
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010

1111
import java.io.IOException;
1212
import java.io.InputStream;
13+
import java.io.UncheckedIOException;
1314
import java.util.List;
1415
import java.util.Map;
1516
import java.util.Map.Entry;
1617
import java.util.concurrent.CompletableFuture;
18+
import java.util.function.Predicate;
1719
import java.util.stream.Collectors;
1820
import javax.annotation.Nullable;
1921
import org.opensearch.client.ApiClient;
@@ -24,14 +26,37 @@
2426
* Client for the generic HTTP requests.
2527
*/
2628
public class OpenSearchGenericClient extends ApiClient<OpenSearchTransport, OpenSearchGenericClient> {
29+
/**
30+
* Generic client options
31+
*/
32+
public static final class ClientOptions {
33+
private static final ClientOptions DEFAULT = new ClientOptions();
34+
35+
private final Predicate<Integer> error;
36+
37+
private ClientOptions() {
38+
this(statusCode -> false);
39+
}
40+
41+
private ClientOptions(final Predicate<Integer> error) {
42+
this.error = error;
43+
}
44+
45+
public static ClientOptions throwOnHttpErrors() {
46+
return new ClientOptions(statusCode -> statusCode >= 400);
47+
}
48+
}
49+
2750
/**
2851
* Generic endpoint instance
2952
*/
3053
private static final class GenericEndpoint implements org.opensearch.client.transport.GenericEndpoint<Request, Response> {
3154
private final Request request;
55+
private final Predicate<Integer> error;
3256

33-
public GenericEndpoint(Request request) {
57+
public GenericEndpoint(Request request, Predicate<Integer> error) {
3458
this.request = request;
59+
this.error = error;
3560
}
3661

3762
@Override
@@ -67,24 +92,70 @@ public GenericResponse responseDeserializer(
6792
int status,
6893
String reason,
6994
List<Entry<String, String>> headers,
70-
String contentType,
71-
InputStream body
95+
@Nullable String contentType,
96+
@Nullable InputStream body
7297
) {
73-
return new GenericResponse(uri, protocol, method, status, reason, headers, Body.from(body, contentType));
98+
if (isError(status)) {
99+
// Fully consume the response body since the it will be propagated as an exception with possible no chance to be closed
100+
try (Body b = Body.from(body, contentType)) {
101+
if (b != null) {
102+
return new GenericResponse(
103+
uri,
104+
protocol,
105+
method,
106+
status,
107+
reason,
108+
headers,
109+
Body.from(b.bodyAsBytes(), b.contentType())
110+
);
111+
} else {
112+
return new GenericResponse(uri, protocol, method, status, reason, headers);
113+
}
114+
} catch (final IOException ex) {
115+
throw new UncheckedIOException(ex);
116+
}
117+
} else {
118+
return new GenericResponse(uri, protocol, method, status, reason, headers, Body.from(body, contentType));
119+
}
120+
}
121+
122+
@Override
123+
public boolean isError(int statusCode) {
124+
return error.test(statusCode);
125+
}
126+
127+
@Override
128+
public <T extends RuntimeException> T exceptionConverter(int statusCode, @Nullable Response error) {
129+
throw new OpenSearchClientException(error);
74130
}
75131
}
76132

133+
private final ClientOptions clientOptions;
134+
77135
public OpenSearchGenericClient(OpenSearchTransport transport) {
78-
super(transport, null);
136+
this(transport, null, ClientOptions.DEFAULT);
79137
}
80138

81139
public OpenSearchGenericClient(OpenSearchTransport transport, @Nullable TransportOptions transportOptions) {
140+
this(transport, transportOptions, ClientOptions.DEFAULT);
141+
}
142+
143+
public OpenSearchGenericClient(
144+
OpenSearchTransport transport,
145+
@Nullable TransportOptions transportOptions,
146+
ClientOptions clientOptions
147+
) {
82148
super(transport, transportOptions);
149+
this.clientOptions = clientOptions;
150+
}
151+
152+
public OpenSearchGenericClient withClientOptions(ClientOptions clientOptions) {
153+
return new OpenSearchGenericClient(this.transport, this.transportOptions, clientOptions);
83154
}
84155

85156
@Override
86157
public OpenSearchGenericClient withTransportOptions(@Nullable TransportOptions transportOptions) {
87-
return new OpenSearchGenericClient(this.transport, transportOptions);
158+
return new OpenSearchGenericClient(this.transport, transportOptions, this.clientOptions);
88159
}
89160

90161
/**
@@ -94,7 +165,7 @@ public OpenSearchGenericClient withTransportOptions(@Nullable TransportOptions t
94165
* @throws IOException I/O exception
95166
*/
96167
public Response execute(Request request) throws IOException {
97-
return transport.performRequest(request, new GenericEndpoint(request), this.transportOptions);
168+
return transport.performRequest(request, new GenericEndpoint(request, clientOptions.error), this.transportOptions);
98169
}
99170

100171
/**
@@ -103,6 +174,6 @@ public Response execute(Request request) throws IOException {
103174
* @return generic HTTP response future
104175
*/
105176
public CompletableFuture<Response> executeAsync(Request request) {
106-
return transport.performRequestAsync(request, new GenericEndpoint(request), this.transportOptions);
177+
return transport.performRequestAsync(request, new GenericEndpoint(request, clientOptions.error), this.transportOptions);
107178
}
108179
}

Diff for: java-client/src/main/java/org/opensearch/client/transport/Endpoint.java

+11
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
import javax.annotation.Nullable;
3838
import org.opensearch.client.json.JsonpDeserializer;
3939
import org.opensearch.client.json.NdJsonpSerializable;
40+
import org.opensearch.client.opensearch._types.ErrorResponse;
41+
import org.opensearch.client.opensearch._types.OpenSearchException;
4042

4143
/**
4244
* An endpoint links requests and responses to HTTP protocol encoding. It also defines the error response
@@ -90,4 +92,13 @@ default Map<String, String> headers(RequestT request) {
9092
@Nullable
9193
JsonpDeserializer<ErrorT> errorDeserializer(int statusCode);
9294

95+
/**
96+
* Converts error response to exception instance of type {@code T}
97+
* @param <T> exception type
98+
* @param error error response
99+
* @return exception instance
100+
*/
101+
default <T extends RuntimeException> T exceptionConverter(int statusCode, @Nullable ErrorT error) {
102+
throw new OpenSearchException((ErrorResponse) error);
103+
}
93104
}

Diff for: java-client/src/main/java/org/opensearch/client/transport/httpclient5/ApacheHttpClient5Transport.java

+56-25
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
import org.apache.hc.core5.http.HttpEntity;
6363
import org.apache.hc.core5.http.HttpHost;
6464
import org.apache.hc.core5.http.HttpRequest;
65+
import org.apache.hc.core5.http.HttpStatus;
6566
import org.apache.hc.core5.http.io.entity.BufferedHttpEntity;
6667
import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
6768
import org.apache.hc.core5.http.io.entity.EntityUtils;
@@ -76,8 +77,6 @@
7677
import org.opensearch.client.json.JsonpDeserializer;
7778
import org.opensearch.client.json.JsonpMapper;
7879
import org.opensearch.client.json.NdJsonpSerializable;
79-
import org.opensearch.client.opensearch._types.ErrorResponse;
80-
import org.opensearch.client.opensearch._types.OpenSearchException;
8180
import org.opensearch.client.transport.Endpoint;
8281
import org.opensearch.client.transport.GenericEndpoint;
8382
import org.opensearch.client.transport.GenericSerializable;
@@ -485,36 +484,68 @@ private <ResponseT, ErrorT> ResponseT prepareResponse(Response clientResp, Endpo
485484

486485
try {
487486
int statusCode = clientResp.getStatusLine().getStatusCode();
488-
489-
if (endpoint.isError(statusCode)) {
490-
JsonpDeserializer<ErrorT> errorDeserializer = endpoint.errorDeserializer(statusCode);
491-
if (errorDeserializer == null) {
492-
throw new TransportException("Request failed with status code '" + statusCode + "'", new ResponseException(clientResp));
493-
}
494-
487+
if (statusCode == HttpStatus.SC_FORBIDDEN) {
488+
throw new TransportException("Forbidden access", new ResponseException(clientResp));
489+
} else if (statusCode == HttpStatus.SC_UNAUTHORIZED) {
490+
throw new TransportException("Unauthorized access", new ResponseException(clientResp));
491+
} else if (endpoint.isError(statusCode)) {
495492
HttpEntity entity = clientResp.getEntity();
496493
if (entity == null) {
497494
throw new TransportException("Expecting a response body, but none was sent", new ResponseException(clientResp));
498495
}
499496

500-
// We may have to replay it.
501-
entity = new BufferedHttpEntity(entity);
502-
503-
try {
504-
InputStream content = entity.getContent();
505-
try (JsonParser parser = mapper.jsonProvider().createParser(content)) {
506-
ErrorT error = errorDeserializer.deserialize(parser, mapper);
507-
// TODO: have the endpoint provide the exception constructor
508-
throw new OpenSearchException((ErrorResponse) error);
497+
if (endpoint instanceof GenericEndpoint) {
498+
@SuppressWarnings("unchecked")
499+
final GenericEndpoint<?, ResponseT> rawEndpoint = (GenericEndpoint<?, ResponseT>) endpoint;
500+
501+
final RequestLine requestLine = clientResp.getRequestLine();
502+
final StatusLine statusLine = clientResp.getStatusLine();
503+
504+
// We may have to replay it.
505+
entity = new BufferedHttpEntity(entity);
506+
507+
try (InputStream content = entity.getContent()) {
508+
final ResponseT error = rawEndpoint.responseDeserializer(
509+
requestLine.getUri(),
510+
requestLine.getMethod(),
511+
requestLine.getProtocolVersion().format(),
512+
statusLine.getStatusCode(),
513+
statusLine.getReasonPhrase(),
514+
Arrays.stream(clientResp.getHeaders())
515+
.map(h -> new AbstractMap.SimpleEntry<String, String>(h.getName(), h.getValue()))
516+
.collect(Collectors.toList()),
517+
entity.getContentType(),
518+
content
519+
);
520+
throw rawEndpoint.exceptionConverter(statusCode, error);
509521
}
510-
} catch (MissingRequiredPropertyException errorEx) {
511-
// Could not decode exception, try the response type
522+
} else {
523+
JsonpDeserializer<ErrorT> errorDeserializer = endpoint.errorDeserializer(statusCode);
524+
if (errorDeserializer == null) {
525+
throw new TransportException(
526+
"Request failed with status code '" + statusCode + "'",
527+
new ResponseException(clientResp)
528+
);
529+
}
530+
531+
// We may have to replay it.
532+
entity = new BufferedHttpEntity(entity);
533+
512534
try {
513-
ResponseT response = decodeResponse(statusCode, entity, clientResp, endpoint);
514-
return response;
515-
} catch (Exception respEx) {
516-
// No better luck: throw the original error decoding exception
517-
throw new TransportException("Failed to decode error response", new ResponseException(clientResp));
535+
InputStream content = entity.getContent();
536+
try (JsonParser parser = mapper.jsonProvider().createParser(content)) {
537+
ErrorT error = errorDeserializer.deserialize(parser, mapper);
538+
throw endpoint.exceptionConverter(statusCode, error);
539+
}
540+
} catch (MissingRequiredPropertyException errorEx) {
541+
// Could not decode exception, try the response type
542+
try {
543+
ResponseT response = decodeResponse(statusCode, entity, clientResp, endpoint);
544+
return response;
545+
} catch (Exception respEx) {
546+
// No better luck: throw the original error decoding exception
547+
throw new TransportException("Failed to decode error response", new ResponseException(clientResp));
548+
}
518549
}
519550
}
520551
} else {

0 commit comments

Comments
 (0)