Skip to content

Commit a675781

Browse files
Fix RestClientOptions building to not lose builtin headers (#380) (#382)
Co-authored-by: Sylvain Wallez <[email protected]>
1 parent 485f2e1 commit a675781

File tree

5 files changed

+291
-36
lines changed

5 files changed

+291
-36
lines changed

java-client/src/main/java/co/elastic/clients/ApiClient.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import co.elastic.clients.json.JsonpMapperBase;
2727

2828
import javax.annotation.Nullable;
29+
import java.util.function.Function;
2930

3031
public abstract class ApiClient<T extends Transport, Self extends ApiClient<T, Self>> {
3132

@@ -52,15 +53,27 @@ protected <V> JsonpDeserializer<V> getDeserializer(Class<V> clazz) {
5253
*/
5354
public abstract Self withTransportOptions(@Nullable TransportOptions transportOptions);
5455

56+
/**
57+
* Creates a new client with additional request options
58+
*
59+
* @param fn a lambda expression that takes the current options as input
60+
*/
61+
public Self withTransportOptions(Function<TransportOptions.Builder, TransportOptions.Builder> fn) {
62+
return withTransportOptions(fn.apply(_transportOptions().toBuilder()).build());
63+
}
64+
5565
/**
5666
* Get the transport used by this client to handle communication with the server.
5767
*/
5868
public T _transport() {
5969
return this.transport;
6070
}
6171

72+
/**
73+
* Get the transport options used for this client. If the client has no custom options, falls back to the transport's options.
74+
*/
6275
public TransportOptions _transportOptions() {
63-
return this.transportOptions;
76+
return this.transportOptions == null ? transport.options() : transportOptions;
6477
}
6578

6679
/**

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

+34-30
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import co.elastic.clients.transport.TransportOptions;
2323
import co.elastic.clients.transport.Version;
24+
import co.elastic.clients.util.VisibleForTesting;
2425
import org.apache.http.impl.nio.client.HttpAsyncClientBuilder;
2526
import org.apache.http.util.VersionInfo;
2627
import org.elasticsearch.client.RequestOptions;
@@ -38,6 +39,14 @@ public class RestClientOptions implements TransportOptions {
3839

3940
private final RequestOptions options;
4041

42+
private static final String CLIENT_META_HEADER = "X-Elastic-Client-Meta";
43+
private static final String USER_AGENT_HEADER = "User-Agent";
44+
45+
@VisibleForTesting
46+
static final String CLIENT_META_VALUE = getClientMeta();
47+
@VisibleForTesting
48+
static final String USER_AGENT_VALUE = getUserAgent();
49+
4150
static RestClientOptions of(TransportOptions options) {
4251
if (options instanceof RestClientOptions) {
4352
return (RestClientOptions)options;
@@ -52,7 +61,7 @@ static RestClientOptions of(TransportOptions options) {
5261
}
5362

5463
public RestClientOptions(RequestOptions options) {
55-
this.options = options;
64+
this.options = addBuiltinHeaders(options.toBuilder()).build();
5665
}
5766

5867
/**
@@ -109,23 +118,13 @@ public RequestOptions.Builder restClientRequestOptionsBuilder() {
109118

110119
@Override
111120
public TransportOptions.Builder addHeader(String name, String value) {
112-
if (name.equalsIgnoreCase(CLIENT_META)) {
121+
if (name.equalsIgnoreCase(CLIENT_META_HEADER)) {
122+
// Not overridable
113123
return this;
114124
}
115-
if (name.equalsIgnoreCase(USER_AGENT)) {
116-
// We must filter out our own user-agent from the options or they'll end up as multiple values for the header
117-
RequestOptions options = builder.build();
118-
builder = RequestOptions.DEFAULT.toBuilder();
119-
options.getParameters().forEach((k, v) -> builder.addParameter(k, v));
120-
options.getHeaders().forEach(h -> {
121-
if (!h.getName().equalsIgnoreCase(USER_AGENT)) {
122-
builder.addHeader(h.getName(), h.getValue());
123-
}
124-
});
125-
builder.setWarningsHandler(options.getWarningsHandler());
126-
if (options.getHttpAsyncResponseConsumerFactory() != null) {
127-
builder.setHttpAsyncResponseConsumerFactory(options.getHttpAsyncResponseConsumerFactory());
128-
}
125+
if (name.equalsIgnoreCase(USER_AGENT_HEADER)) {
126+
// We must remove our own user-agent from the options, or we'll end up with multiple values for the header
127+
builder.removeHeader(USER_AGENT_HEADER);
129128
}
130129
builder.addHeader(name, value);
131130
return this;
@@ -159,32 +158,37 @@ public TransportOptions.Builder onWarnings(Function<List<String>, Boolean> liste
159158

160159
@Override
161160
public RestClientOptions build() {
162-
return new RestClientOptions(builder.build());
161+
return new RestClientOptions(addBuiltinHeaders(builder).build());
163162
}
164163
}
165164

166-
private static final String USER_AGENT = "User-Agent";
167-
private static final String CLIENT_META = "X-Elastic-Client-Meta";
168-
169165
static RestClientOptions initialOptions() {
170-
String ua = String.format(
166+
return new RestClientOptions(RequestOptions.DEFAULT);
167+
}
168+
169+
private static RequestOptions.Builder addBuiltinHeaders(RequestOptions.Builder builder) {
170+
builder.removeHeader(CLIENT_META_HEADER);
171+
builder.addHeader(CLIENT_META_HEADER, CLIENT_META_VALUE);
172+
if (builder.getHeaders().stream().noneMatch(h -> h.getName().equalsIgnoreCase(USER_AGENT_HEADER))) {
173+
builder.addHeader(USER_AGENT_HEADER, USER_AGENT_VALUE);
174+
}
175+
if (builder.getHeaders().stream().noneMatch(h -> h.getName().equalsIgnoreCase("Accept"))) {
176+
builder.addHeader("Accept", RestClientTransport.JsonContentType.toString());
177+
}
178+
179+
return builder;
180+
}
181+
182+
private static String getUserAgent() {
183+
return String.format(
171184
Locale.ROOT,
172185
"elastic-java/%s (Java/%s)",
173186
Version.VERSION == null ? "Unknown" : Version.VERSION.toString(),
174187
System.getProperty("java.version")
175188
);
176-
177-
return new RestClientOptions(
178-
RequestOptions.DEFAULT.toBuilder()
179-
.addHeader(USER_AGENT, ua)
180-
.addHeader(CLIENT_META, getClientMeta())
181-
.addHeader("Accept", RestClientTransport.JsonContentType.toString())
182-
.build()
183-
);
184189
}
185190

186191
private static String getClientMeta() {
187-
188192
VersionInfo httpClientVersion = null;
189193
try {
190194
httpClientVersion = VersionInfo.loadVersionInfo(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
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.util;
21+
22+
import java.lang.annotation.ElementType;
23+
import java.lang.annotation.Retention;
24+
import java.lang.annotation.RetentionPolicy;
25+
import java.lang.annotation.Target;
26+
27+
/**
28+
* Annotates a program element that exists, or is more widely visible than otherwise necessary, only
29+
* for use in test code.
30+
*
31+
* <p><b>Do not use this interface</b> for public or protected declarations: it is a fig leaf for
32+
* bad design, and it does not prevent anyone from using the declaration---and experience has shown
33+
* that they will.
34+
*
35+
* <p>Borrowed from <a href="https://github.com/google/guava/blob/master/guava/src/com/google/common/annotations/VisibleForTesting.java">
36+
* Guava</a>.
37+
*/
38+
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE})
39+
@Retention(RetentionPolicy.SOURCE)
40+
public @interface VisibleForTesting {
41+
}

java-client/src/test/java/co/elastic/clients/transport/RequestOptionsTest.java

+13-5
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,15 @@ private Properties getProps(ElasticsearchClient client) throws IOException {
100100
return result;
101101
}
102102

103+
@Test
104+
public void testNonNullClientOptions() {
105+
final RestClientTransport trsp = new RestClientTransport(restClient, new JsonbJsonpMapper());
106+
final ElasticsearchClient client = new ElasticsearchClient(trsp);
107+
108+
assertNotNull(client._transportOptions());
109+
assertSame(trsp.options(), client._transportOptions());
110+
}
111+
103112
@Test
104113
public void testDefaultHeaders() throws IOException {
105114
final RestClientTransport trsp = new RestClientTransport(restClient, new JsonbJsonpMapper());
@@ -111,7 +120,7 @@ public void testDefaultHeaders() throws IOException {
111120
assertTrue(props.getProperty("header-x-elastic-client-meta").contains("es="));
112121
assertTrue(props.getProperty("header-x-elastic-client-meta").contains("hl=2"));
113122
assertEquals(
114-
"application/vnd.elasticsearch+json; compatible-with=" + String.valueOf(Version.VERSION.major()),
123+
"application/vnd.elasticsearch+json; compatible-with=" + Version.VERSION.major(),
115124
props.getProperty("header-accept")
116125
);
117126
}
@@ -120,10 +129,9 @@ public void testDefaultHeaders() throws IOException {
120129
public void testClientHeader() throws IOException {
121130
final RestClientTransport trsp = new RestClientTransport(restClient, new JsonbJsonpMapper());
122131
final ElasticsearchClient client = new ElasticsearchClient(trsp)
123-
.withTransportOptions(trsp.options().with(
124-
b -> b.addHeader("X-Foo", "Bar")
125-
.addHeader("uSer-agEnt", "MegaClient/1.2.3")
126-
)
132+
.withTransportOptions(b -> b
133+
.addHeader("X-Foo", "Bar")
134+
.addHeader("uSer-agEnt", "MegaClient/1.2.3")
127135
);
128136

129137
Properties props = getProps(client);

0 commit comments

Comments
 (0)